Java
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
182: Defining Modules with module-info.java
Wait, what actually goes inside a module-info.java file?
Think of the module-info.java file as the "manifest" or the boundary fence for your code. It's a special file that must sit at the root of your source folder. Instead of just letting every public class be visible to the entire world, you explicitly declare who you need and who can see you.
Let's say we're building a payment system. We have a module called com.payment.core. Here is what a basic definition looks like:
module com.payment.core {
requires java.sql; // I need the SQL module to talk to the database
exports com.payment.core.api; // Only classes in this package are visible to other modules
}
The requires keyword tells the JVM, "I can't run without this other module." The exports keyword is where the real power is; it tells the JVM, "I'm allowing other modules to use the classes in this specific package." If a package isn't exported, it's effectively private to the module, even if the classes inside it are marked public.
Why is my code not compiling even though the class is public?
This is the most common frustration when moving to the Java Module System. In the old days, if a class was public, any other class on the classpath could touch it. Not anymore. Now, you have two layers of security: the public modifier and the exports clause.
I've seen developers spend an hour debugging this. They have a class PaymentProcessor marked as public in the package com.payment.core.internal, but they forgot to export that package in module-info.java. To the outside world, that class is now invisible.
If you want another module—say, com.payment.app—to use your processor, you must explicitly export it:
module com.payment.core {
exports com.payment.core.internal;
}
My advice? Keep your exports list as small as possible. Only export the API packages that other people need to see. Hide your implementation details in non-exported packages.
What is the point of "requires transitive"?
Sometimes you have a dependency chain. Imagine our com.payment.core module uses a third-party logging module called com.logger.util. Now, imagine another module, com.payment.app, depends on com.payment.core. If the app needs to use those logging tools too, it would normally have to requires com.logger.util explicitly.
That gets tedious if you have a deep stack of dependencies. That's where requires transitive comes in. It's basically saying, "If you depend on me, you get this other module for free."
module com.payment.core {
requires transitive com.logger.util;
exports com.payment.core.api;
}
Now, when com.payment.app adds requires com.payment.core;, it automatically gains access to com.logger.util. Use this sparingly, though. If you over-use it, you end up creating a giant web of dependencies that's just as messy as the classpath we were trying to escape.
📋 Practical Task
Exercise: Implementing a Modular Currency Converter
Your task is to set up a modular project consisting of two modules: com.converter.engine and com.converter.cli.
- Module 1 (com.converter.engine):
- Create a package
com.converter.engine.apicontaining a public classCurrencyServicewith a methodconvert(double amount). - Create a package
com.converter.engine.internalcontaining a public classExchangeRateProvider. - Create a
module-info.javathat exportscom.converter.engine.apibut does not exportcom.converter.engine.internal.
- Create a package
- Module 2 (com.converter.cli):
- Create a
module-info.javathatrequires com.converter.engine. - Create a Main class that attempts to instantiate
CurrencyService(this should work). - Create a line of code that attempts to instantiate
ExchangeRateProvider.
- Create a
Goal: Verify that the project compiles when accessing CurrencyService, but fails with a compilation error when you try to access ExchangeRateProvider, proving that your module boundaries are correctly enforced.
There are no comments for now.