While the Composite pattern does result in a tree structure, that's not its primary motivation. It's not the one true wayTM to build a tree. It's purpose is to provide a way to treat the composite object the same way as an individual object (what would be a node, from a heterogeneous set of nodes).
Let me try and shift your thinking a bit from the tree metaphor. Imagine drawing shapes in a Word document (boxes, circles, triangles, arrows). These are individual objects that you can position, rotate, scale, etc. Now draw a bunch of them, and select the command to group them. Now you can position/rotate/scale the group as a whole. That group is a composite. That's something that fits perfectly with the Composite pattern.
If your goal is specifically to work with a more general purpose tree structure, then feel free to stray away from the pattern. If a single recursive class will do it, go with that. Have leaf nodes implement a way to return the child count, and return an empty list if child nodes are requested (this lets you avoid null checks, and null-related bugs). This is probably the way to go for what you're trying to do.
If your tree is somewhat a special-purpose tree that supports a limited set of operations, where you can just ask the tree to do things for you (instead of getting the individual nodes, reading their properties, and doing stuff with the nodes/tree in the calling code), then the Composite pattern might be a better fit.
For example, take the shape group mentioned above; think about how you'd implement the Draw operation. (My code snippets are going to use C# syntax, but just think of them as of pseudocode).
Inside the Composite class, you'd just forward the Draw call to each child (maybe after doing some offsets and rotations):
class Composite : IComponent {
private List<IComponent> children = new List<IComponent>();
// there's some way to add child shapes to the group (e.g. the ctor)
// ...
public Draw() {
foreach(var child in children) {
child.Draw();
}
}
}
This will then polymorphically call the Draw() method down the tree branches until a leaf is reached. A leaf is any of the individual concrete shape classes:
class Arrow : IComponent {
public Draw() {
// use some graphics API to draw an arrow
}
}
Note that at no point there was a check to see if it's a composite or a leaf node. The polymorphic operations execute in the context of the object on which they are called, and the object itself knows if it's a leaf or not.
The calling code just looks like:
group.Draw(); // the entire group draws itself
You only have to check for the type/leaf if your operations are unable to make use of polymorphism, and are implemented outside of the tree structure, in the calling code (client code):
Draw(group);
// where Draw is implemented something like this:
void Draw(IComponent shape) {
if (shape.ChildCount > 0) {
foreach(var child in shape.Children) {
Draw(child);
}
}
else if (shape is Rectangle) {
// cast, extract data, draw rectangle
}
else if (shape is Circle) {
// cast, extract data, draw circle
}
else if (shape is Arrow) {
// cast, extract data, draw arrow
}
else if... {
// ...
}
}
So, the two approaches make a tradeoff - one favors a special-purpose tree with a limited set of "smart" operations, the other a general-purpose bare-bones tree with only generic tree-related operations, leaving anything beyond that for the client code to implement. With the first one, it's easy to add new kinds of shapes, but it's not as easy to add new operations after-the-fact (e.g. if you published this as a library, and other people can derive new shapes). With the second one, the code is messier, and possibly error-prone and less expressive. And adding operations is easier than adding shapes (cause you'd have to hunt down every method to add a new case).
You can also opt for some combination of the two - have it lean more towards a general-purpose tree, but support a couple of useful smart operations. What's useful will depend on what your code/application is for.
For more advanced approaches (that can be an overkill, and come with their own set of tradeoffs), look into the Iterator and the Visitor patterns. Java Streams Philipp's answer mentions are a version of the Iterator pattern (what's called an internal iterator).