Aug 2, 2012

Access to protected and default (package) members in Java

Regarding visibility, protected is one of the most misunderstood modifiers in Java. It's always tricky to get it right, and when combined with the default (or package) visibility, it can become really confusing. Here is my attempt to cover every single possibility of these two access levels. Let's start with some code:
package com.svpino.snippets.white;

public class Parent {
    void method0() {
        System.out.println("Parent::method0");
    }
 
    protected void method1() {
        System.out.println("Parent::method1");
    }
}
Here we are defining two methods, method0 has default visibility (since no modifier is specified), and method1 is protected. So far, so good. Let's see now the other class:
package com.svpino.snippets.white;

public class Child1 extends Parent {
 
    public void method2() {
        System.out.println("Child1::method2");
        method0();
        method1();
    }
}
The class Child1 is inheriting from Parent and its located on the same package. This means, that lines 7 and 8 are perfectly valid. Line 7 can be executed because method0 has package visibility and Child1 is located on the same com.svpino.snippets.white package as Parent. Line 8 is valid because method1 is protected so Child1 can access it through inheritance. But, what if we try these methods from a different package? Let's see:
package com.svpino.snippets.black;

import com.svpino.snippets.white.Parent;

public class Child2 extends Parent {
 
    public void method2() {
        System.out.println("Child2::method2");
        method0(); // Error! This doesn't compile!
        method1();
    }
}
Now in this case, Line 9 is not valid anymore. We can't call method0 because we are on a different package than Parent. However, Line 10 is still valid since we are accessing a protected member through inheritance. Now let's spice things up a little bit:
package com.svpino.snippets.white;

import com.svpino.snippets.black.Child2;

public class Neighbor1 {

    public void method3() {
        Parent parent = new Parent();
        parent.method0();
        parent.method1();

        Child1 child1 = new Child1();
        child1.method0();
        child1.method1();
        child1.method2();

        Child2 child2 = new Child2();
        child2.method0(); // Error! This doesn't compile!
        ((Parent) child2).method0();
        child2.method1();
        child2.method2();
    }
}
Line 9 has no problem to compile, since we are on the same package than Parent. Line 10 also compiles (and this is interesting) since the protected visibility in Java works the same as the package visibility. Keep that in mind all the time: A protected member has essentially package-level or default access to all classes except for subclasses. Moving to the next block, Line 13 and Line 14 will also compile for the same reasons Line 9 and 10 compile respectively. Line 14 is calling a public method from Child1 so it will also compile. Nothing fancy here. Let's see what happens in the last block. Line 18 doesn't compile because we are trying to access method0 through the Child2 class. If you go back to that class you'll see it's defined in a different package than Neighbor1, and because method0 has default visibility, it won't be visible at this point. However, casting child2 to a Parent will let us access method0. The compiler will validate that we do have access to everything defined with package visibility in the Parent class, so Line 19 won't be a problem. Now, think about Line 20. It will compile, but why? It was kind of shocking for me discovering this. Supposedly, method1 should behave exactly as method0 since package visibility acts like default visibility, however there's another subtle difference between them. I had to check the Java specification to find the key:
6.6.1 (...) if the member or constructor is declared protected, then access is permitted only when one of the following is true: * Access to the member or constructor occurs from within the package containing the class in which the protected member or constructor is declared. (...)
Now compare that to the following about default access:
6.6.1 (...) Otherwise, we say there is default access, which is permitted only when the access occurs from within the package in which the type is declared.(...)
So the difference is that a protected member will be always accessible when the access occurs from within the package where the class containing the member was declared, meanwhile default access is only possible when the access occurs from the same package where the type (the type of the object we are using to access the member) was declared. In our case, the member method1 was declared in the package com.svpino.snippets.white, and we are accessing it from Neighbor1 which happens to be on the same package, so the access will be allowed even though we are using Child2 (the type) to do it. It sounds complicated, and I had to read it more than once myself to make sense out of it. But if you pay careful attention to the wording, you'll see it. Finally, it's not surprising than Line 21 will compile since method2 has public access. The code for our last test is as follows:
package com.svpino.snippets.black;

import com.svpino.snippets.white.Child1;
import com.svpino.snippets.white.Parent;

public class Neighbor2 {

    public void method3() {
        Parent parent = new Parent();
        parent.method0(); // Error! This doesn't compile!
        parent.method1(); // Error! This doesn't compile!

        Child1 child1 = new Child1();
        child1.method0(); // Error! This doesn't compile!
        child1.method1(); // Error! This doesn't compile!
        child1.method2();

        Child2 child2 = new Child2();
        child2.method0(); // Error! This doesn't compile!
        ((Parent) child2).method0(); // Error! This doesn't compile!
        child2.method1(); // Error! This doesn't compile!
        child2.method2();
    }
}
Lines 10 and 11 will not compile since we are accessing them from outside the package where they were defined. Same thing happens with lines 14 and 15, meanwhile line 16 is a simple public access. Now, line 19 is trying to access a default member that was defined in a different package, so it won't compile. The cast in line 20 won't compile because Parent still belongs to a different package, and look how interesting is line 21. We can't access method1 even though it is inherited by Child2 from Parent. The reason is because once a subclass outside the package inherits a protected member, that member becomes private to any code outside the subclass with the exception of subclasses of the subclass. Yes, another tongue-twister, so you might want to read it again. Basically, as soon as method1 is inherited, its visibility becomes private to any outsider (like our Neighbor2 class). So that's about it. Impressive how tricky these modifiers can be, specially the protected access. It took some time to wrap my head around all these concepts, but now I have it pretty clear. Hope it helps.

No comments:

Post a Comment