DEV Community

Mayur Bodhare
Mayur Bodhare

Posted on

Java Packages: Why Your Folders Must Match Your Code

For a long time, the first line of my Java files was a mystery. Some files started with package com.something..., some didn't, and when I moved a file to another folder, everything broke. Then there was the second mystery: I wrote a method in one class and got an error calling it from another, even though both were "in my project."

Both mysteries have the same root. Java wants you to answer two questions about your code: where does this class live? (packages) and who is allowed to use it? (access modifiers).

In this post, we'll learn both, one at a time, and then see them work together. I've drawn the important pictures in text so you can see how everything connects.

Part 1: Packages

What is a package?

A package is a group of related classes, interfaces and enums. The easiest way to think of it is as a folder for your classes.

Packages solve three problems:

  • Organization. A big project has hundreds of classes. Packages keep them sorted (shop, payments, users).
  • Name clashes. Two classes can have the same name if they're in different packages. Java's own library has a Date in java.util and another Date in java.sql.
  • Access control. Packages decide who can see what (more on this in Part 2).

Declaring a package

You declare a package with the package statement at the very top of the file:

package com.example.shop;

public class Order {
    public void place() {
        System.out.println("Order placed");
    }
}
Enter fullscreen mode Exit fullscreen mode

By convention, package names are all lowercase and start with a reversed domain name, like com.example.shop. That keeps them unique worldwide.

The package name and the folder structure must match. This file belongs in com/example/shop/Order.java:

src/
  com/
    example/
      shop/
        Order.java        <- package com.example.shop;
      app/
        Main.java         <- package com.example.app;
Enter fullscreen mode Exit fullscreen mode

Using a class from another package

To use Order from a different package, you import it:

package com.example.app;

import com.example.shop.Order;

public class Main {
    public static void main(String[] args) {
        Order order = new Order();
        order.place();
    }
}
Enter fullscreen mode Exit fullscreen mode

To compile and run this from the src folder:

javac -d out com/example/shop/Order.java com/example/app/Main.java
java -cp out com.example.app.Main
Enter fullscreen mode Exit fullscreen mode

Output:

Order placed
Enter fullscreen mode Exit fullscreen mode

Notice that we run the class by its full name, com.example.app.Main, not just Main. We'll see why in a moment.

Behind the scenes: what a package name really is

A class's real name includes its package. Order is really com.example.shop.Order. That's called its fully qualified name.

When you compile with -d out, javac creates matching folders and puts the .class files inside them. When the JVM needs a class, it turns the dots into slashes and looks for that path on the classpath (the list of places it searches).

   com.example.shop.Order
          |
          |  dots become folder separators
          v
   out/com/example/shop/Order.class
          ^
          |
   found by searching the classpath (here, the "out" folder)
Enter fullscreen mode Exit fullscreen mode

This is why the full name is needed to run, and why a wrong folder layout gives errors like "Could not find or load main class."

What does import actually do?

Many beginners think import copies code into the file. It doesn't. import is only a typing shortcut that the compiler understands. Without it, you'd write the full name every time:

com.example.shop.Order order = new com.example.shop.Order();
Enter fullscreen mode Exit fullscreen mode

The import line just lets you write Order instead. Nothing is loaded or included because of it. The class is loaded later, when your code actually uses it.

A few related things:

  • Single import: import java.util.List; brings in one class.
  • Wildcard import: import java.util.*; brings in every class in that package, but not classes in its sub-packages (java.util.concurrent needs its own import).
  • java.lang is automatic. That's why you can use String, System and Math with no import.

When two classes have the same name

If you need both Date classes, use the full names:

System.out.println(java.util.Date.class.getName());
System.out.println(java.sql.Date.class.getName());
Enter fullscreen mode Exit fullscreen mode

Output:

java.util.Date
java.sql.Date
Enter fullscreen mode Exit fullscreen mode

Two wildcard imports can also clash. If you write import java.util.*; and import java.sql.*;, then using plain Date gives an "ambiguous" compile error, because Java can't tell which one you mean. Importing the one you need by name solves it.

The default (unnamed) package

If you write no package statement, your class goes into the default package. That's fine for tiny practice programs. In real projects, avoid it, because classes in a named package can't import classes from the default package.

Part 2: Access modifiers

Now the second question: who is allowed to use a class or a member (a field, method or constructor)?

Java has four levels. Here they are from the tightest to the widest:

  • private: visible only inside the same class.
  • default (you write nothing): visible inside the same package. It's also called package-private.
  • protected: visible inside the same package, plus subclasses, even if they're in other packages.
  • public: visible everywhere.

Each level includes everything the one before it allows, so you can picture them as rings:

 +------------------------------------------------------------+
 | public: everyone                                           |
 |  +------------------------------------------------------+  |
 |  | protected: same package + subclasses anywhere        |  |
 |  |  +------------------------------------------------+  |  |
 |  |  | default: same package                          |  |  |
 |  |  |  +------------------------------------------+  |  |  |
 |  |  |  | private: same class only                 |  |  |  |
 |  |  |  +------------------------------------------+  |  |  |
 |  |  +------------------------------------------------+  |  |
 |  +------------------------------------------------------+  |
 +------------------------------------------------------------+
Enter fullscreen mode Exit fullscreen mode

And here's the same thing as a quick lookup. Read each row as "who can access it?":

               same class | same package | subclass (other pkg) | everyone else
 private           yes    |      no      |         no           |      no
 (default)         yes    |     yes      |         no           |      no
 protected         yes    |     yes      |        yes           |      no
 public            yes    |     yes      |        yes           |     yes
Enter fullscreen mode Exit fullscreen mode

Where can you use them?

  • Members (fields, methods, constructors) can use all four.
  • Top-level classes can only be public or default. A top-level class can't be private or protected.
  • Local variables inside methods have no access modifier at all.
  • A file can have only one public class, and the file must be named after it.

Let's see all four in action

We'll use four classes in three packages. Here's who is trying to reach whom:

  com.example.bank                  com.example.savings        com.example.app
 +---------------------+           +------------------+       +----------------+
 | Account             | <-extends-| SavingsAccount   |       | Main           |
 | Auditor             |           +------------------+       | (unrelated)    |
 +---------------------+                                      +----------------+
Enter fullscreen mode Exit fullscreen mode

First, the Account class, with one field of each level:

package com.example.bank;

public class Account {
    private double balance = 1000;
    String branch = "Pune";
    protected String owner = "Asha";
    public String bankName = "Demo Bank";

    public double getBalance() {
        return balance;
    }
}
Enter fullscreen mode Exit fullscreen mode

1. A class in the same package (Auditor).

package com.example.bank;

public class Auditor {
    public void check(Account a) {
        System.out.println(a.bankName);      // public: OK
        System.out.println(a.owner);         // protected: OK (same package)
        System.out.println(a.branch);        // default: OK (same package)
        // System.out.println(a.balance);    // private: ERROR
    }
}
Enter fullscreen mode Exit fullscreen mode

2. A subclass in a different package (SavingsAccount).

package com.example.savings;

import com.example.bank.Account;

public class SavingsAccount extends Account {
    public void show() {
        System.out.println(bankName);        // public: OK
        System.out.println(owner);           // protected: OK (inherited)
        // System.out.println(branch);       // default: ERROR (other package)
        // System.out.println(balance);      // private: ERROR
    }
}
Enter fullscreen mode Exit fullscreen mode

3. An unrelated class in a different package (Main).

package com.example.app;

import com.example.bank.Account;
import com.example.savings.SavingsAccount;

public class Main {
    public static void main(String[] args) {
        Account acc = new Account();
        System.out.println(acc.bankName);        // public: OK
        System.out.println(acc.getBalance());    // OK, through a public method
        // System.out.println(acc.owner);        // protected: ERROR (not a subclass)
        // System.out.println(acc.branch);       // default: ERROR
        // System.out.println(acc.balance);      // private: ERROR

        new SavingsAccount().show();
    }
}
Enter fullscreen mode Exit fullscreen mode

Output:

Demo Bank
1000.0
Demo Bank
Asha
Enter fullscreen mode Exit fullscreen mode

If you remove the comment marks from the lines marked ERROR, the compiler stops you. The messages look like this:

balance has private access in com.example.bank.Account
branch is not public in com.example.bank.Account; cannot be accessed from outside package
owner has protected access in com.example.bank.Account
Enter fullscreen mode Exit fullscreen mode

Notice how Main still got the balance, through getBalance(). The field is private, but the public method is the gate. That's encapsulation, and access modifiers are the tool that makes it possible.

The tricky one: protected

People often think protected means "subclasses only." It actually means "same package plus subclasses." And for a subclass in another package, there's a catch: it can use the protected member through itself, but not through a plain reference to the parent:

package com.example.savings;

import com.example.bank.Account;

public class SavingsAccount extends Account {
    public void peek(Account other) {
        System.out.println(this.owner);     // OK: my own inherited member
        System.out.println(other.owner);    // ERROR: protected access through an Account reference
    }
}
Enter fullscreen mode Exit fullscreen mode

The idea is that you inherited owner as part of being a SavingsAccount. That doesn't give you the right to look inside every other Account object.

Behind the scenes: who checks the rules?

The compiler checks access when you compile, which is what produced those error messages. The JVM checks again when the program runs, as a second line of defense (for example, if two class files that don't match are put together).

The access level is stored in the .class file itself. You can see it with javap:

javap -p -cp out com.example.bank.Account
Enter fullscreen mode Exit fullscreen mode

The -p flag shows private members too. You'll see keywords like private, protected and public next to each field. A member with no keyword is package-private. (Advanced tools like reflection can bypass private in normal code, and Java's module system adds a further layer of control, but that's for another day.)

Access modifiers and other features

  • Overriding: a child can't make an inherited method less visible. A public parent method can't become protected in the child.
  • Interfaces: methods in an interface are public automatically. So when a class implements one, the method must be written public, or you get "attempting to assign weaker access privileges."
  • Constructors: a private constructor stops outside code from creating objects. It's used for utility classes and the singleton pattern.

Which one should I use?

My simple rule is to start with the tightest level and open up only when you must. Here's the reasoning: everything you make public becomes a promise to everyone who uses it. The smaller that promise, the freer you are to change your code later.

  • private for fields, almost always. Give access through methods when the outside world needs it.
  • private for helper methods that only make sense inside the class.
  • Default (package-private) for classes and members that should be shared inside one package but hidden from the rest, like internal helper classes.
  • protected for members designed to be used or overridden by subclasses. Use it carefully, because it's also visible to the whole package.
  • public for the part of your class that you want other code to use: your "official" methods and the classes that are the entry point of your package.

Common mistakes

  1. Thinking "no modifier" means private. It doesn't. No modifier means package-private, so every class in the same package can see it.
  2. Making everything public to "get rid of errors." It works, but it removes the protection and makes later changes risky. Fix the design instead.
  3. A package name that doesn't match the folder. The result is "class not found" errors or NoClassDefFoundError. Also remember to run java com.example.app.Main (full name), not java Main.
  4. Believing import copies code or includes sub-packages. It's only a naming shortcut, and import java.util.*; doesn't cover java.util.concurrent.
  5. Misunderstanding protected. It includes the whole package, and in another package it works only through the subclass itself, not through a parent-type reference.
  6. Forgetting public when implementing an interface method. Interface methods are public, and your version can't be less visible. The compiler complains about "weaker access privileges."

Quick recap

  • A package is a folder-like group of classes. Names are lowercase and use a reversed domain, like com.example.shop.
  • The package name must match the folder path. The JVM turns dots into slashes to find the .class file on the classpath.
  • import is a typing shortcut. It doesn't copy or load anything. java.lang is imported automatically.
  • The four access levels, tightest to widest: private (same class), default (same package), protected (same package + subclasses), public (everyone).
  • Top-level classes can only be public or default.
  • The compiler enforces access, and the JVM double-checks it.
  • Start tight, and open up only when needed.

Try it yourself

Here's a small practice idea: recreate the Account, Auditor, SavingsAccount and Main classes from this post in three packages. Compile them, then remove the comment marks from the "ERROR" lines one at a time and read each error message. After that, change one field's modifier (for example, make branch protected) and predict which classes will start to compile before you test.

For a bonus experiment, run javap -p on your compiled Account class and compare what you see with the access modifiers you wrote.

If this helped, or if you spotted something I should fix, tell me in the comments. I'm still learning too.

Top comments (0)