"I'm going to tell you about «access modifiers». I told about them once before, but repetition is a pillar of learning."
You can control the access (visibility) that other classes have to the methods and variables of your class. An access modifier answers the question «Who can access this method/variable?». You can specify only one modifier for each method or variable.
1) «public» modifier.
A variable, method, or class marked with the public modifier can be accessed from anywhere in the program. This is the highest degree of openness: there are no restrictions.
2) «private» modifier.
A variable, method, or class marked with the private modifier can only be accessed in the class where it's declared. The marked method or variable is hidden from all other classes. This is the highest degree of privacy: accessible only by your class. Such methods are not inherited and cannot be overridden. Additionally, they cannot be accessed in a descendent class.
3) «Default modifier».
If a variable or method is not marked with any modifier, then it is considered to be marked with the "default" modifier. Variables and methods with this modifier are visible to all classes in the package where they are declared, and only to those classes. This modifier is also called "package" or "package private" access, hinting at the fact that access to variables and methods is open to the entire package that contains the class.
4) «protected» modifier.
This level of access is slightly broader than package. A variable, method, or class marked with the protected modifier can be accessed from its package (like "package"), and from all inherited classes.
This table explains it all:
Type of visibility | Keyword | Access | |||
---|---|---|---|---|---|
Your class | Your package | Descendent | All classes | ||
Private | private | Yes | No | No | No |
Package | (no modifier) | Yes | Yes | No | No |
Protected | protected | Yes | Yes | Yes | No |
Public | public | Yes | Yes | Yes | Yes |
There's a way to easily remember this table. Imagine that you're writing a will. You're dividing all your things into four categories. Who gets to use your things?
Who has access | Modifier | Example |
---|---|---|
Just me | private | Personal journal |
Family | (no modifier) | Family photos |
Family and heirs | protected | Family estate |
Everybody | public | Memoirs |
"It's a lot like imagining that classes in the same package are part of one family."
"I also want to tell you some interesting nuances about overriding methods."
1) Implicit implementation of an abstract method.
Let's say you have the following code:
class Cat
{
public String getName()
{
return "Oscar";
}
}
And you decided to create a Tiger class that inherits this class, and add an interface to the new class
class Cat
{
public String getName()
{
return "Oscar";
}
}
interface HasName
{
String getName();
int getWeight();
}
class Tiger extends Cat implements HasName
{
public int getWeight()
{
return 115;
}
}
If you just implement all the missing methods that IntelliJ IDEA tells you to implement, later you might end up spending a long time searching for a bug.
It turns out that the Tiger class has a getName method inherited from Cat, which will be taken as the implementation of the getName method for the HasName interface.
"I don't see anything terrible about that."
"It's not too bad, it a likely place for mistakes to creep in."
But it can be even worse:
interface HasWeight
{
int getValue();
}
interface HasSize
{
int getValue();
}
class Tiger extends Cat implements HasWeight, HasSize
{
public int getValue()
{
return 115;
}
}
It turns out that you can't always inherit from multiple interfaces. More precisely, you can inherit them, but you can't implement them correctly. Look at the example. Both interfaces require that you implement the getValue() method, but it's not clear what it should return: the weight or the size? This is quite unpleasant to have to deal with.
"I agree. You want to implement a method, but you can't. You've already inherited a method with the same name from the base class. It's broken."
"But there's good news."
2) Expanding visibility. When you inherit a type, you can expand the visibility of a method. This is how it looks:
Java code | Description |
---|---|
|
|
|
We've expanded the method's visibility from protected to public . |
Code | Why this is «legal» |
---|---|
|
Everything's great. Here we don't even know that the visibility has been extended in a descendant class. |
|
Here we call the method whose visibility has been extended.
If this weren't possible, we could always declare a method in Tiger: In other words, we're not talking about any security violation. |
|
If all the conditions necessary to call a method in a base class (Cat) are satisfied, then they are certainly satisfied for calling the method on the descendent type (Tiger) . Because the restrictions on the method call were weak, not strong. |
"I'm not sure I completely understood, but I'll remember that this is possible."
3) Narrowing the return type.
In an overridden method, we can change the return type to a narrowed reference type.
Java code | Description |
---|---|
|
|
|
We overrode the method getMyParent , and now it returns a Tiger object. |
Code | Why this is «legal» |
---|---|
|
Everything's great. Here we don't even know that the getMyParent method's return type has been widened in the descendant class.
How the «old code» worked and works. |
|
Here we call the method whose return type has been narrowed.
If this weren't possible, we could always declare a method in Tiger: In other words, there are no security violations and/or type casting violations. |
|
And everything works fine here, though we widened the variables' type to the base class (Cat).
Because of overriding, the correct setMyParent method is called. And there is nothing to worry about when calling the getMyParent method, because the return value, though of the Tiger class, can still be assigned to the myParent variable of the base class (Cat) without any problems. Tiger objects can be safely stored both in Tiger variables and Cat variables. |
"Yep. Got it. When overriding methods, you have to be aware of how all this works if we pass our objects to code that can only handle the base class and doesn't know anything about our class."
"Exactly! Then the big question is why can't we narrow the return value's type when overriding a method?"
"It's obvious that in this case the code in the base class would stop working:"
Java code | Explanation of the problem |
---|---|
|
|
|
We overloaded the getMyParent method and narrowed the type of its return value.
Everything is fine here. |
|
Then this code will stop working.
The getMyParent method can return any instance of an Object, because it is actually called on a Tiger object. And we don't have a check before the assignment. Thus, it is entirely possible that the Cat-type myParent variable will store a String reference. |
"Wonderful example, Amigo!"
In Java, before a method is called, there is no check whether the object has such a method. All checks occur at runtime. And a [hypothetical] call to a missing method would most likely cause the program to attempt to execute non-existent bytecode. This would ultimately lead to a fatal error, and the operating system would forcibly close the program.
"Whoa. Now I know."
GO TO FULL VERSION