Scope
I did terrible (34%) on test questions of chapter 12 of the book, basically because I missed fundamentals about scope. Somehow I had not given attention to chapter 2 and 8 and I was still unfamiliar with differences between overriding, shadowing and hiding.
This blog is entirely about scope and I’ll do this by reading chapter 6 paragraph 3 of the Java Language Specification version 11. Here the rules for all sorts of scope are layed out. I will illustrate every line I read with a code example or some comment. I will skip some lines if they deal with things higher than top-level types (packages, imports etc).
“The scope of a declaration is the region of the program within which the entity declared by the declaration can be referred to using a simple name, provided it is not shadowed (§6.4.1).”
Here the term shadowed is used for the first time. The ‘a simple name’ term is interesting, for example with enum constants. Those are mostly not accessible by their simple name outside of their own type body, with an exception for case statements.
“The scope of a top level type (§7.6) is all type declarations in the package in which the top level type is declared.”
If you create a class, interface or enum, it is accessible within all other classes, interfaces and enums of the same package.
“The scope of a declaration of a member m declared in or inherited by a class type C (§8.1.6) is the entire body of C, including any nested type declarations.”
The members of a class (fields and methods) are accessible anywhere in the class body, even in the most nested parts (think of local methods), by their simple name, unless they are shadowed. Example below is correct, members can be called anywhere if done correctly. It is still important that calling instance variables or instance methods from a static context requires an instance of the outer class.
public class Scope {
static String myStaticString = "myStaticString";
private Integer myInstanceNumber = 27;
{myInstanceNumber = 28;}
void addOne(){myInstanceNumber++;}
private class Inner{
private class InnerInner{
final int a = 2 + myInstanceNumber;
void doIt(){
addOne();
System.out.println();
class InnerInnerLocal{
{ addOne();}
int b = myInstanceNumber;
}}}
}
static void someMethod(){
Scope myMS = new Scope();
System.out.println(myStaticString + myMS.myInstanceNumber);
}
}
“The scope of a declaration of a member m declared in or inherited by an interface type I (§9.1.4) is the entire body of I, including any nested type declarations.”
This is identical. I wondered what was meant by nested type declarations in the case of interfaces, but you actually can create local classes and static nested classes. The following snippet is an interface with all six member types that are allowed in an interface (public static final class variable, public abstract method, public default method, public static method, private static method and private instance method.
I added an inner static class to it and a local class. In the inner class I shadowed the public final static variable myString, just to practice. All members (fields and methods) are in scope everywhere, unless when they are shadowed (which happens in static inner class Inner at the end).
public interface ScopeInterface {
String myString = "MyString";
void firstAbstractMethod();
default String adjustString(String a){
String b = helpDefaultMethod(a);
return b;
}
private String helpDefaultMethod(String a){
class localClass {
void localClassMethod(){ System.out.println(myString);}
}
return (myString + "_adjusted");
}
static void myStaticMethod(int a){
myStaticMethodHelper(a);
}
private static void myStaticMethodHelper(int a){
System.out.println(myString + a);
}
static class Inner{
void innerClassMethod(){
String myString = "A nested String"; // shadowing a member variable
System.out.println(ScopeInterface.myString + " - " + myString);
}
}
}
“The scope of an enum constant C declared in an enum type T is the body of T, and any case label of a switch statement whose expression is of enum type T (§14.11).”
The book pointed out that in switch-case statements, you must use the enum constant without the enum class prefix after case (case TUESDAY:
instead of case Days.TUESDAY:
). Apparently this is the only occassion where you can use an enum constant without the prefix. (Btw in nowadays Java both versions are allowed).
Within the body of the enum type declaration itself you can, logically, use the enum constant as well. In the review questions of chapter 12 (question 2), the following snippet was presented:
public class FlavorsEnum{
enum Flavors{
VANILLA, CHOCOLATE, STRAWBERRY;
static final Flavors DEFAULT = STRAWBERRY;
}
public static void main(String... ha){
// some code
}
}
I was confused about the line static final Flavors DEFAULT = STRAWBERRY;
but this is an example where an enum constant (STRAWBERRY) is used with its simple name within the body of the enum type. It is thus perfectly valid.
“The scope of a formal parameter of a method (§8.4.1), constructor (§8.8.1), or lambda expression (§15.27) is the entire body of the method, constructor, or lambda expression.”
As I didn’t give much attention to scope, I noticed that I lived under the assumption that as class and instance variables could hide inherited variables by redeclaring them, and local variables could shadow instance and class variables by redeclaring them, the parameters of lamdbdas, constructors and methods could be hidden as well by redeclaring them. This is not true, they cannot be hidden or shadowed at all. The following snippet contains 3 compile errors:
public class Scope {
int a;
Scope(int a){
int a = 5; // not allowed
this.a = a;
}
Predicate<Integer> predicate = b->{
int b = 3; // not allowed
return b>0;
};
void myMethod(int c){
int c = 2; // not allowed
System.out.println(predicate.test(c));
}
}
You cannot redeclare the parameters of lambdas, methods and constructors, as it results in compile errors.
“The scope of a class’s type parameter (§8.1.2) is the type parameter section of the class declaration, the type parameter section of any superclass or superinterface of the class declaration, and the class body.”
I just made something very complicated with generics in it. T and U are available throughout the whole class body. I didn’t get into the type parameters of the superclass or superinterface.
public class Box <T,U> {
T thing1;
U thing2;
public Box(T thing1, U thing2){
this.thing1 = thing1;
this.thing2 = thing2;
}
public T getThing1(){
return thing1;
}
public U getThing2(){
return thing2;
}
public void printNames(T t, U u){
class Inner {
BiConsumer<T,U> printer = (x,y)->{
System.out.printf("%s - %s", x, y);
};
void print(T x, U y){
printer.accept(x,y);
}
}
Inner inner = new Inner();
inner.print(t,u);
}
public static void main(String[] args){
Box<String, Integer> myBox = new Box<>("Hello", 57463);
myBox.printNames(myBox.getThing1(), myBox.getThing2());
}
}
// output "Hello - 57463"
“The scope of an interface’s type parameter (§9.1.2) is the type parameter section of the interface declaration, the type parameter section of any superinterface of the interface declaration, and the interface body.”
Here I could make an example that illustrates the second part of this rule: the type parameter section of any superinterface of the interface declaration.. It is not an interface implementing an interface but a class implementing an interface but the principle is the same.
Note that R and Q have no scope relationship whatsoever. How could they, they might reside in completely different places.
public interface CanPrint <R>{ // this R
void print(R item); // must correspond with this R. Same scope.
}
class Printer<Q> implements CanPrint<Q>{ // these two Q's are in the same scope
public void print(Q item){ // and this one also is
System.out.println(item);
}
public static void main(String... args){
new Printer<String>().print("Hello");
}
}
“The scope of a method’s type parameter (§8.4.4) is the entire declaration of the method, including the type parameter section, but excluding the method modifiers.”
Here an example inspired by Perplexity. It’s not shocking:
public class Utility {
public <K> void print(K[] someArray){
for (K item : someArray)
System.out.println(item);
}
public static void main(String[] args){
Double[] list = {10.21, 3.67,21.970};
new Utility().print(list);
}
}
“The scope of a constructor’s type parameter (§8.8.4) is the entire declaration of the constructor, including the type parameter section, but excluding the constructor modifiers.”
I leave this one unillustrated.
“The scope of a local class declaration immediately enclosed by a block (§14.2) is the rest of the immediately enclosing block, including its own class declaration.”
It took me some time to figure out what was meant exactly but now it dawns on me. This has to do with the order in which the code is written inside a method. While Java is indifferent to the order of class members are declared and initialized, Java behaves differently within a method body. If you want to use a local variable, a or a local class you can only do that after the variable or class is declared/initialized. My guess is that this has to do with the fact that method bodies are processed later than class members.
The point is that you can access the local class object everywhere in the enclosing method body but only after the class has been declared.
public class Party {
void someMethod(){
LocalClass no1 = new LocalClass(); // this one doesn't compile. Compiler doesn't know LocalClass type
class LocalClass { // from here on you have access to this object type
LocalClass no2 = new LocalClass(); // this works
}
LocalClass no3 = new LocalClass(); // this works
class LocalClass2 {
LocalClass no4 = new LocalClass(); // this works
}
}
}
“The scope of a local class declaration immediately enclosed by a switch block statement group (§14.11) is the rest of the immediately enclosing switch block statement group, including its own class declaration.”
Below the illustration. You can create a block after a case:
and in his block you can write as much as you want, including inner classes. From the point on where you declared the inner class, you can create an instance of it.
public class MySwitch {
public static void choice(int switchValue) {
switch(switchValue){
case 1: { // case statement with brackets
System.out.println("1");
class MyClass {
MyClass myClass = new MyClass();
}
MyClass myClass = new MyClass();
break;
}
case 2:
System.out.println("2");
break;
default:
System.out.println("Some other value");
}
}
}
“The scope of a local variable declaration in a block (§14.4) is the rest of the block in which the declaration appears, starting with its own initializer and including any further declarators to the right in the local variable declaration statement.”
Simple example with an instance initializer block, but works with any block. Order matters.
public class BlockClass {
{
var b = 3 * a; // doesn't compile. "Cannot resolve symbol 'a'"
var a = 10;
}
}
“The scope of a local variable declared in the ForInit part of a basic for statement (§14.14.1) includes all of the following:”
- Its own initializer
- Any further declarators to the right in the ForInit part of the for statement
- The Expression and ForUpdate parts of the for statement
- The contained Statement
Explanation of terms: a basic for statement has the following structure:
for ( [ForInit] ; [Expression] ; [ForUpdate] ) Statement
The statement can be between brackets or not, if it is just a single statement. Essentially i and j in the following sample have a scope that ends at the last bracket of the loop-body.
public class ForClass {
void someMethod(){
for (int i=2, j=0; i<5 && j<8;i++, j++){
int i = 5; // doesn't compile
System.out.println(i+j);
}
int i = 28; // fine, this is another variable.
}
}
“The scope of a local variable declared in the FormalParameter part of an enhanced for statement (§14.14.2) is the contained Statement.”
Similar to previous one
“The scope of a parameter of an exception handler that is declared in a catch clause of a try statement (§14.20) is the entire block associated with the catch.”
Basically, don’t try to reassign the value e:
public class TryCatch {
void someMethod(){
try {
// Block of code to try
}
catch(Exception e) {
Exception e = new RuntimeException(); // doesn't compile
// "variable 'e' is already defined in the scope.
}
}
}
The scope of a variable declared in the ResourceSpecification of a try-with-resources statement (§14.20.3) is from the declaration rightward over the remainder of the ResourceSpecification and the entire try block associated with the try-with-resources statement.”
The structure of a try-with-resources statement has the following structure:
try ResourceSpecification Block [Catches] [Finally]
The resource specification is the part between parentheses where you open files etc. Any variable declared here has a scope that goes from declaration to the end of the try block. The variables of the resource specification and those of the try block are not accessible in the catch block or in the finally block.
public class TryWithResources {
void someMethod(){
String doodle = "Initialized local variable"
String line; // better: String line = null;
try (BufferedReader bf = new BufferedReader(new FileReader("text.txt"))){
String warning = "Help";
while ((line=bf.readLine())!=null){ // Beautiful. Assignment returns assigned value.
System.out.println(line);
};
}
catch(IOException e) {
System.out.println(bf); // Does not compile. Variable from resource specification not accessible.
System.out.println(warning); // Does not compile. Variable from try block not accessible.
System.out.println(line); // Does not compile. Variable line is in scope but might not have been initialized. Compiler intervenes.
System.out.println(doodle); // fine. Doodle is an initialized local variable.
}
}
}
I stop this blog post here, the next paragraph in the language specification (6.4) is about Shadowing and Obscuring. I don’t know what the latter means.