Краткое содержание

В Java можно объявлять переменные в классе, методе, блоке или конструкторе. В зависимости от варианта объявления, у переменной могут быть разные области видимости в программе, а сама переменная может относиться к статическому или нестатическому типу в зависимости от того, где она объявлена.

Основываясь на этих критериях, можно разделить все переменные в Java на следующие типы: локальные переменные, переменные экземпляра и переменные класса. Затенение (shadowing) и сокрытие (hiding) переменных происходит, когда оказываются одноименными  две переменные в разных областях видимости (в локальной и глобальной области видимости, в родительских и дочерних классах). При таком сценарии используется значение той переменной, которая находится во внутренней области видимости, так как она затемняет/скрывает значение той переменной, что находится во внешней области видимости.

Введение

Рассмотрим нижеприведенный пример, где определяется две переменные с именем id. Можно предположить, что следующая программа выбросит ошибку, так как мы объявили две переменные с одним и тем же именем id. Но, к счастью, никаких ошибок не возникнет, а мы получим вывод 5678. Хотя обе переменные и имеют одно и то же имя id, они объявляются в разных областях видимости: одна в глобальной, а другая в локальной.

Переменная id в глобальной области видимости доступна из любой точки внутри класса, тогда как переменная id в локальной области видимости доступна только из метода main(). Вывод равен  5678, поскольку здесь происходит затенение переменной: переменная id внутри метода main() «заслоняет» глобальную переменную id и использует собственное значение.

class Main {
    // глобальная область видимости
    String id = "1234";

    public static void main(String[] args) {
        // локальная область видимости
        String id = "5678";
        
        System.out.println(id);
    }
}

Вывод

5678

Чтобы разобраться с затенением и сокрытием переменных, сначала нужно изучить различные типы переменных, имеющиеся в Java: как и где они объявляются, какова их область видимости и применения, т.д. Все переменные Java можно подразделить на три типа.

  • Локальные переменные

  • Переменные экземпляра

  • Глобальные переменные

Локальные переменные

Переменные, объявляемые внутри метода, блока или конструктора, называются локальными. Область видимости локальной переменной ограничена пределами того метода, блока или конструктора, в которой она создана. Локальные переменные создаются, когда программа переходит к выполнению метода, блока или конструктора, и уничтожаются, как только выполнение закончено. Локальные переменные нельзя объявлять как статические.

public int findSum(int[] arr) {
    // локальная переменная
    int sum = 0;

    for (final int j : arr) {
        sum += j;
    }
        
    return sum;
}

В этом примере sum – это локальная переменная, так как она объявлена внутри метода findSum(), и извне этого метода к ней обратиться нельзя. 

Переменные экземпляра

Переменные, объявляемые внутри класса, но вне метода называются переменными экземпляра. Переменные экземпляра не объявляются как статические. Переменные экземпляра создаются при создании объекта и уничтожаются при уничтожении объекта.

class Student {
    // переменная экземпляра
    private final String name;
    
    public Student(String name) {
        this.name = name;
    }
    
    public String getName() {
        return this.name;
    }
}

В данном примере name – это переменная экземпляра, поскольку она объявляется внутри класса, но вне метода. Новая переменная name создается для всех вновь создаваемых объектов Student.

Переменные класса

Переменные, объявляемые как статические внутри класса, но вне метода, называются переменными класса. Переменные класса объявляются как статические.

В каждом класса любая переменная класса может существовать всего в одном экземпляре, независимо от того, в каком количестве создаются объекты. Переменные класса создаются при запуске программы и уничтожаются, когда программа завершается. Переменные класса также называются статическими.

class Variable {
    // переменная класса
    static int classVariable;
    int instanceVariable;

    public Compare() {
        int localVariable;
    }
}

В данном примере classVariable – это переменная класса, так как она объявляется внутри класса, но вне метода, и при этом объявляется как статическая. Переменная classVariable совместно используется всеми созданными объектами Variable.

Что такое затенение переменных?

Затенение переменных происходит, когда переменная во внутренней области видимости объявляется с таким же именем, с каким уже существует переменная во внешней области видимости. В данном случае переменная во внутренней области видимости затемняет (маскирует) переменную во внешней области видимости. Затенение переменных происходит даже в том случае, когда обе одноименные переменные относятся к разным типам данных.

Пример затемнения переменных

В примере, приведенном ниже, у нас две переменные с именем name. Одна из них находится внутри метода print() (внутренняя область видимости), а другая внутри класса Student (внешняя область видимости). Метод print выводит имя Steve Rogers, а не Tony Stark, поскольку имя локальной переменной внутри метода print() (это внутренняя область видимости) затемняет переменную экземпляра name, находящуюся внутри класса Student (внешняя область видимости). Поэтому на экран выводится значение затеняющей переменной – той, что находится в методе print().

class Student {
    // переменная экземпляра
    String name = "Tony Stark";

    public void print() {
        // локальная переменная
        String name = "Steve Rogers";
        System.out.println(name);
    }
}

public class Main {
    public static void main(String[] args) {
        Student student = new Student();
        student.print();
    }
}

Вывод

Steve Rogers

Затенение локальными переменными

Затенение локальными переменными происходит, когда локальная переменная (во внутренней области видимости) затемняет переменную экземпляра (во внешней области видимости).

class A {
    // переменная экземпляра
    int x = 1;

    public void display() {
        // локальная переменная
        int x = 2;
        
        System.out.println(x);
        System.out.println(this.x);
    }
}

public class Main {
    public static void main(String[] args) {
        A a = new A();
        a.display();
    }
}

Вывод

2
1

В данном примере у нас две переменных (одна внутри метода display(), а другая внутри класса A), которые объявлены с одним и тем же именем x. При выводе значения той x, что находится в методе display(). На экран выводится 2, так как локальная переменная x внутри метода display() затемняет переменную экземпляра x внутри класса A.

Затенение аргументом метода

Затенение аргументом метода происходит, когда параметр метода (внутренняя область видимости) затемняет переменную экземпляра (внешняя область видимости).

class A {
    // instance variable
    int x = 1;
    
    // method parameter
    public void display(int x) {
        System.out.println(x);
        System.out.println(this.x);
    }
}
public class Main {
    public static void main(String[] args) {
        A a = new A();
        a.display(2);
    }
}

Вывод

2
1

В данном примере параметр метода display() и переменная экземпляра класса A имеют одинаковое имя x. Выводя значение того x, что находится внутри метода display(), видим на экране 2, так как параметр x метода display() затемняет переменную экземпляра x, находящуюся в классе A.

Что такое сокрытие переменной?

Сокрытие переменной происходит в случае, когда у переменной, объявленной в дочернем классе, имя такое же, как и у переменной, объявленной в родительском классе.

Напротив, затенение переменной происходит в случае, когда переменная во внутренней области видимости является одноименной переменной во внешней области видимости. Переменные из дочернего класса обычно скрывают переменные родительского класса при такой одноименности. Сокрытие переменной происходит даже в том случае, когда обе одноименные переменные относятся к разным типам данных.

Пример сокрытия переменных

class Parent {
    String name = "ParentClass";

    public void display() {
        System.out.println(name);
    }
}

class Child extends Parent {
    String name = "ChildClass";

    @Override
    public void display() {
        System.out.println(name);
    }

    public void displayParent() {
        System.out.println(super.name);
    }
}

public class Main {
    public static void main(String[] args) {
        Child child = new Child();
        child.display();
        child.displayParent();
    }
} 

Вывод

ChildClass
ParentClass

В данном примере показано, что переменная name в классе Child скрывает переменную name в классе Parent. Когда вызывается метод отображения в классе Child, он выводит на экран ChildClass. Ключевое слово super должно использоваться для доступу к переменной name в классе Parent.

Сокрытие в случае со статическими переменными

Статическое сокрытие происходит между двумя одноименными переменными, одна из которых относится к родительскому классу, а другая – к дочернему, причем, если перед обеими в качестве префикса стоит ключевое слово static. Статические переменные также называются переменными класса.

class Parent {
    static String id = "1";
    static String name = "ParentClass";

    public void displayName() {
        System.out.println(name);
    }
    
    public void displayId() { 
        System.out.println(id); 
    }
}

class Child extends Parent {
    static int id = 2;
    static String name = "ChildClass";

    @Override
    public void displayName() {
        System.out.println(name);
    }

    @Override
    public void displayId() {
        System.out.println(id);
    }
}

public class Main {
    public static void main(String[] args) {
        Parent parent = new Parent();
        Child child = new Child();

        parent.displayId(); 
        parent.displayName();
        child.displayId();
        child.displayName();
    }
}

Вывод

1
ParentClass
2
ChildClass

В данном примере и в классе Parent, и в классе Child есть две переменные класса с именами id и name. Хотя, переменные дочернего класса являются одноименными переменным родительского класса, методы displayId и displayName из дочернего класса выводят данные, относящиеся именно к этому классу. Здесь переменные класса Child скрывают переменные класса Parent. Сокрытие затрагивает даже переменную id, которая в родительском и дочернем классе является одноименной, но при этом в первом и втором случае соответствует разным типам.

Сокрытие нестатических переменных

Сокрытие нестатических переменных происходит, когда две переменные (одна в родительском классе, одна в дочернем) объявляются с одинаковым именем и при этом не имеют ключевого слова static в качестве префикса. Нестатические переменные также называются переменными экземпляра.

class Parent {
    String name = "ParentClass";

    public void displayName() {
        System.out.println(name);
    }
}

class Child extends Parent {
    String name = "ChildClass";

    @Override
    public void displayName() {
        System.out.println(name);
    }
}

public class Main {
    public static void main(String[] args) {
        Parent parent = new Parent();
        Child child = new Child();

        parent.displayName();
        child.displayName();
    }
}

Вывод

ParentClass
ChildClass

В данном примере как у класса Parent, так и у класса Child есть нестатическая переменная name. Переменная name в классе Child скрывает переменную name в родительском классе. Когда мы выводим на экран ту переменную name, что относится к методу displayName() класса Child, мы получаем ChildClass, а не ParentClass. Дело в том, что переменная name в классе Child скрывает значение той name, что находится в классе Parent, и использует собственное значение.

Сокрытие переменной – это не то же самое, что переопределение метода

Переопределение метода – это возможность, при которой дочерний класс меняет ту реализацию метода, которую получил из родительского класса, собственной реализацией, а при сокрытии переменной реализация просто прячется.

class Parent {
    public void print() {
        System.out.println("I am Parent");
    }
}

class Child extends Parent {
    @Override
    public void print() {
        System.out.println("I am Child");   
    }
}

public class Main {
    public static void main(String[] args) {
        Parent parent = new Parent();
        Child child = new Child();

        Parent childAsParent = new Child();

        parent.displayName();
        child.displayName();
        childAsParent.displayName();
    }
}

Вывод

I am Parent
I am Child
I am Child

В данном примере класс Child переопределяет реализацию метода print из класса Parent (выводит I am Parent) собственной реализацией (выводит I am Child).

При сокрытии переменные дочернего класса скрывают только переменные родительского класса. С другой стороны, при переопределении методы дочернего класса заменяют методы родительского класса. Рассмотрим приведенный ниже пример, где объект childAsParent типа Parent указывает на объект типа Child.

class Parent {
    public String name = "ParentClass";

    public void displayName() {
        System.out.println(name);
    }
}

class Child extends Parent {
    public String name = "ChildClass";

    @Override
    public void displayName() {
        System.out.println(name);
    }
}

public class Main {
    public static void main(String[] args) {
        Parent childAsParent = new Child();
        
        System.out.println(childAsParent.name); 

        childAsParent.displayName();
    }
}

Вывод

ParentClass
ChildClass

При сокрытии переменных переменные дочернего класса скрывают переменные родительского класса. Так что, когда мы обращаемся к переменной по ссылке из родительского класса, которая держит дочерний объект, осуществляется доступ к переменной родительского класса. Вот почему childAsParent.name выводит ParentClass.

При переопределении методы дочернего класса заменяют методы родительского класса. Так что, когда мы обращаемся к методу по ссылке из родительского класса, которая держит дочерний объект, происходит доступ к методу дочернего класса. Вот почему childAsParent.displayName() выводит ChildClass.

Как получить доступ к скрытой или затемненной переменной?

Доступ к затемненной переменной

Локальная переменная затемняет переменную экземпляра, если они одноименные. Ключевое слово this следует использовать для доступа к затемненной переменной экземпляра. В нижеприведенном примере инструкция System.out.println(this.name) выводит значение (Tony Stark), то есть, значение затемненной переменной экземпляра.

class Student {
    // переменная экземпляра
    String name = "Tony Stark"; 

    public void print() {
        // локальная переменная
        String name = "Steve Rogers"; 
        System.out.println(name); // выводит локальную переменную
        System.out.println(this.name); // выводит переменную экземпляра
    }
}

public class Main {
    public static void main(String[] args) {
        Student student = new Student();
        student.print();
    }
}

Вывод

Steve Rogers
Tony Stark

Доступ к скрытой переменной

Переменная дочернего класса скрывает переменную родительского класса, если они одноименные. Ключевое слово super следует использовать для доступа к скрытой переменной родительского класса. В нижеприведенном примере инструкция System.out.println(super.name) выводит значение (ParentClass) скрытой переменной экземпляра, относящейся к родительскому классу.

class Parent {
    String name = "ParentClass";

    public void display() {
        System.out.println(name);
    }
}

class Child extends Parent {
    String name = "ChildClass";

    @Override
    public void display() {
        System.out.println(name);
        System.out.println(super.name);
    }
}

public class Main1 {
    public static void main(String[] args) {
        Child child = new Child();
        child.display();
    }
}

Вывод

ChildClass
ParentClass

Разница между затенением и сокрытием переменных

Затенение переменной

Сокрытие переменной

Затенение переменной происходит, когда одноименными являются локальная переменная и переменная экземпляра в одном и том же классе

Сокрытие переменной происходит, когда переменная из дочернего класса одноименная переменной родительского класса

Затенение переменной происходит внутри класса

Сокрытие переменной происходит между дочерним и родительским классом

Заключение

  • Затенение переменной происходит, когда переменная во внутренней области видимости объявляется с тем же именем, что и переменная из внешней области видимости. Переменная из внешней области видимости затемняет переменную из внутренней области видимости.

  • Затенение локальной переменной происходит, когда локальная переменная во внутренней области видимости затемняет переменную экземпляра во внешней области видимости.

  • Затенение аргументом метода происходит, когда параметр метода затемняет переменную экземпляра.

  • Для доступа к затемненной переменной экземпляра используется ключевое слово this.

  • Сокрытие переменной происходит, когда переменная, объявленная в дочернем классе, имеет то же имя, что и переменная, объявленная в родительском классе.

  • К скрытой переменной из родительского класса можно обратиться при помощи ключевого слова super.

  • Сокрытие переменных – не то же самое, что переопределение метода, поскольку при сокрытии переменные дочернего класса просто прячут переменные родительского класса. В свою очередь, при переопределении метода дочерний класс заменяет реализации методов из родительского класса собственными реализациями.

Комментарии (5)


  1. igorzakhar
    04.04.2022 10:55
    +2

    Первый раз слышу термин "затемнение". Обычно в книгах и статьях встречается перевод "затенение" (shadowing variables).


    1. ph_piter Автор
      04.04.2022 11:49
      +1

      Благодарим вас, исправлено


  1. tsvetkovpa
    04.04.2022 11:49
    +1

    А вообще использование переменных с одинаковыми именем в разных скоупах - признак кода с душком.

    Не надо так делать.


  1. PqDn
    04.04.2022 12:26
    +1

    В заголовок добавить бы "Область видимости". Мне кажется, сами "Сокрытие и затенение" гораздо реже употребляются в этом контексте


  1. vasyakolobok77
    04.04.2022 23:34
    +3

    Неужели вы потратили на "это" целую статью и несколько часов времени писатели и редактора? Это ведь уровень даже не джуна, а пред-джуна.