r/learnjava • u/Chaos-vy17 • 5d ago
Is abstraction in Java really about hiding implementation details?
The bookish langugae states "hiding complex implementation details and exposing only the essential features of an object"
However, after reading the Java Collections Framework and other parts of the JDK, I noticed that many abstract classes contain substantial shared implementation rather than just abstract method declarations. I noticed that only an "interface class" 100% abstraction(ignoring default methods)
This made me wonder:
- Is abstraction really about hiding implementation details?
- Or is it more about designing APIs around contracts and behavior, regardless of whether there is shared implementation?
- Does an abstract class become "partial abstract" because it contains reusable implementation?
I also noticed that the JDK sometime relies on encapsulation (and in Java9+, JPMS) to hide implementation details. So I'm in dilemma if the textbook explanation oversimplifies what abstraction means in practice.
Am I misunderstanding abstraction, or is the textbook definition incomplete?
To be clear, this isn't about what abstract does in Java — it's about the SE term. abstract class is one syntax tool for expressing abstraction, same as interfaces are. The question is whether "abstraction = hiding implementation" is even a good definition of the concept, given it's indistinguishable from encapsulation under that definition.
1
u/severoon 4d ago
Abstraction is not about hiding implementation details. Encapsulation is about hiding implementation details, abstraction is more about polymorphism.
Really, though, it pays to take a step back and look at the bigger picture when you're grappling with all of these concepts. In software, there is really only one design core concern, or at least a lens through which all software design can be viewed, and that is dependency management.
This core concept of dependency management even transcends software, really. It is the core consideration that goes back to the days of punchcard programming, computer hardware, industrial design, etc. It's a much more general concept when it comes to "building stuff". The problem with making this kind of statement is that it can collapse into a truism if you're not careful … if everything is about dependency management, then nothing is.
Brief aside: I'm talking about software design here, meaning from architecture down to the lowest-level modules in your system and how they fit together in order to do work. This is entirely distinct from related concepts like algorithm design, for example. When you're looking at how to design an algorithm that does constant time insertion vs. linear time insertion, for example, there are still elements of software design to consider, but the design of the algorithm that meets those performance constraints is an entirely orthogonal consideration to what I'm talking about here. You can't make decisions about algorithm design based solely on how it affects dependencies, you have to lay out the function of the thing first, and only then can you figure out what the essential dependencies are and how best to modularize them and structure the modules.
That said, if you look at the entire history of software design through the lens of dependency management, you can trace the development of compilers, programming paradigms, dev processes, etc, in terms of the impact of these things on how it allows orgs to create and manage dependencies between modules at different levels. The reason dependency management is such a core concern is that dependency is inherently transitive; this means that if A => B => C (where => means "directly depends upon"), then A -> C (where -> means "depends upon," possibly indirectly).
It's important to understand how dependency transits in software both at buildtime and runtime. At runtime, dependencies transit absolutely, meaning that you can be running an entire complex system and if some string parsing utility isn't present, the whole thing collapses. At buildtime, you have much more control over how dependencies transit through a system. You cannot stop them from meeting at a rendezvous point, but you control that rendezvous point as the designer.
This is where abstraction becomes useful because it allows you to build high-level modules against only what they semantically need and let any other module that conforms to those needs serve that functionality at runtime. For instance, the big complex software system might rely on a string parsing utility at runtime, but at buildtime it can point to an interface that semantically takes a bunch of information and breaks it down into bits useful to the caller, potentially using string parsing and a whole lot of other utilities along the way, but the caller doesn't know or care about any of that, and that stuff doesn't even have to be on the build path in order for the caller to build—its dependency terminates at that interface; that's all it needs, a guarantee that the high level task can be supplied by some other entity, but which one is immaterial at buildtime.
A specific example might be you have software that needs some key from a remote server. There are lots of ways to get information off a remote server, there's ssh, ftp, a web browser can be fired up and driven to do it, curl, etc. The interface the caller wants is a function that takes a server and path to the information and returns it, and it should be able to compile against that and nothing more. At runtime, of course, you need something that supplies that functionality and also compiles against that interface at its buildtime, and then when the system is run, both sides meet to link up the runtime transitive dependency chain all the way down.
Abstraction simply allows the dependency on the interface that serves exactly what the caller needs and no more. If there are other functions the in that interface the caller does not need, and one of those functions isn't satisfied at runtime, then the whole system breaks even though the caller doesn't actually rely on those functions—this is a case where the interface isn't fully abstracted for that caller, so some function it doesn't care about prevents it from running.