Avoid Public Keyword In Your Java Code

Paweł Otorowski
TechTalks@Vattenfall
8 min readJan 5, 2021

After a couple of years working as a software engineer, looking mostly on the Java code, I can now say that many developers overuse public keyword on daily basis.

There might be many reasons for such an approach like wrongly defined default IDE settings or lack of knowledge of how and why the language was designed. Nevertheless, there is always a good opportunity to take a step back and rethink how to use features provided by the language itself.

In this article, I would like to show how overusing public keyword, especially in big projects, may decrease the productivity of the team and how to prevent it. Examples will be written in pure Java, so knowledge of any framework is not needed. In the end, you will find a few tips on how to teach the IntelliJ IDEA to follow “no public” rules.

Scopes in Java

Comparison between the scopes available in Java.

Default (also known as package-private) will be the main topic in the article. This scope is in opposition to the public, required if any class, method, or field should not be visible outside the package. The name speaks for itself, this is the default scope in Java!

Why not public?

There is a couple of reasons why I think we should put more attention when it comes to choosing the right scope for our fields, methods, and classes. In this paragraph, I would like you to take a look at the few examples I created based on the code I have seen in the past. They are pretty simple, but I hope you will visualize how painful it may become letting your IDE decide what should be the right scope for your classes.

Wrong class organization

When everything is public, it is possible to organize all the classes by their responsibility. In the example below, you can find the tree of classes from the project written for some restaurants. There are few packages that describe layers of the application like repository, service or web. In each of these packages, there are classes that fit into them. For example, DB repositories are in the repository package and REST/MVC controllers are in the web package.

In the beginning, it might seem like a good solution, because with such a packaging strategy it should be easy to find a particular endpoint by looking for the class in the web package or to change the query to another one by looking for the class to change in the repository package. On the other hand, the logic spreads between different places, making code harder to understand, especially when focus only on the particular feature is required. When some new functionality has to be added, each package needs to be analyzed to find the right place to implement it. And of course, these packages contain classes written not only for the new feature but also for many more, so the whole project must fit into the developer’s head when the feature is implemented.

Please take a look at the example below. Every class that has open the green padlock is public in this project. Adding a new feature to the existing classes requires going through all of them to find the right place. Also adding a brand new functionality to this project requires going through most of these packages or even all of them to put new classes in the right place.

An example of the project with packages organized by layer.

This problem can be solved by creating packages organized by the functionality and treat them as separate modules. Inside them, all the classes should have a default scope. Thanks to such an approach every time adding a new feature will be required, only a single package needs to be touched or a new one needs to be created. It also gives an easier way to extract any functionality out of the project i.e. to deploy as a separate application to scale it or transfer to a different team.

An example of the project with packages organized by modules.

Problem with finding a class

Modern Java IDEs prompt a class that needs to be imported just by the name typed in the code. It is a really powerful feature that increases developers’ productivity. Unfortunately, it does not work well when all the classes are public. Especially in huge projects, there is a big chance, that more classes have the same name, and only by the package name, it is possible to discover which one is the valid one in each case. It becomes even harder when there is a bug in the code due to the wrongly chosen class and it needs to be found.

In the example below, you can find the tree of classes from the project written for some restaurants. This time modules like client or employee are in place, but inside them, there are still classes organized by their responsibilities: model, repository, web and so on.

An example of the project with packages organized by modules with layers as sub-packages.

Because of such a packaging strategy, it is possible to create classes with the same names. Below there is a prompted list of them that fit the typed part of the class name. As not only employees have an address, but also clients, suppliers, and orders that sometimes need to be delivered, there are multiple AddressRepository classes to select.

An example of class hints to import based on the partial name entered.

This problem can be solved by keeping all the classes from each module in a single separate package with the default scope. With such an approach every time there will be a requirement to add a new dependency to the class, only classes from the same package will be prompted.

An example of exactly one class hint to import based on the partial name entered thanks to the package-private scope.

Spaghetti code

By creating a public class, we make it possible to use it anywhere in the project. There is nothing wrong with it, but only if the decision was conscious. The problem starts in a situation when classes are made public, but they should not be. This leads to the moment in which classes written for functionality A are used for functionalities B and C. The problem with mixing concepts makes providing any changes in the project harder, especially when it comes to extracting any functionality outside the module or application. Unfortunately, it is really easy to introduce, because every public class is available in every place in the project. Check the imports section in the following screenshot:

An example of using OrderRepository from package order in the ClientService from package client.

This issue can be also solved by keeping all the classes from each module in a single separate package with the default scope.

Problem with finding a method

The beauty of the IDEs is that they prompt methods that are available in the API of the class. This is another powerful feature, used on the daily basis by developers all around the world. If there is only one or even a few methods exposed, there should be no issue with using them. This is in line with Abstraction, one of the most important concepts in the OOP (object-oriented programming).

The problem starts when methods that are used for the internal purpose and should not be available outside, are also public. This may lead to confusion about which method should be used and decreases productivity. And of course, there is always a chance that such a method will be used outside the class. It may not only produce various bugs but also make further refactoring harder or even impossible to do.

In the example below there is a lot of methods available in the EmployeeService. Two of them should be public for sure: findBy() and save(). The rest of them seems to be intended to be used only by the public methods of the service itself.

An example of method hints available in the API of the EmployeeService class.

What if communication between modules is needed?

Developing in Java, there might be only one reason to use public in the code - when the point of accessing the module is required. Then the facade pattern can be used to expose any features implemented in the module.

Set up IntelliJ to stop putting public keywords everywhere

The default configuration in the IntelliJ hides visibility of the classes in the project. It also adds a public keyword in almost every generated part of the code. These few steps can help you to start generating code that is consistent with the Java language design. It might also allow you to see what are the visibility scopes in every class of the package without looking into them.

Visibility scope in the project tree

By default, there is no overview of the visibility scopes in the project. The first option is to open each class and see its definition. There is also a better way to achieve this with less effort and it works globally. This option is named Show Visibility Icons. You can find it in the dropdown that is available under the settings button of the project window tool:

Dropdown with a selected option that is responsible for displaying the visibility scope of every class in the project tree.

Generated classes

Every generated class is public by default. It can be easily changed in the IntelliJ settings under Editor > File and Code Templates. To achieve the goal, “public” keyword needs to be removed from Class, Interface, and Enum files.

The class template defines that every auto-generated class should have a default scope.

Generated constructors

Constructors generated by the IntelliJ are public by default. It is configurable in the Code Generation tab that is under the Editor > Code Style > Java settings. Default (package-private) scope is named Package local in that case.

Code Generation tab that contains an option to change the default visibility of the generated constructors.

Summary

The approach I described in this article was not invented by me. It is just a reminder of how the Java language was designed from the very beginning.

Package-private seems to be a very good scope for every newly created class and method in the project. It raises the decision which of them should be visible in our hands. This gives us an opportunity to keep the API of our classes easier to use and understand for everyone.

Thanks for reading and see you next time!

--

--