I. Основы внутренних классов
В Java можно определить класс внутри другого класса или внутри метода. Такой класс называется внутренним классом. В широком смысле внутренние классы обычно включают следующие четыре типа: внутренние классы-члены, локальные внутренние классы, анонимные внутренние классы и статические внутренние классы. Давайте сначала разберёмся с использованием этих четырёх типов внутренних классов.
1. Внутренний класс-член
Внутренний класс-член — наиболее распространённый вид внутреннего класса. Он определяется внутри другого класса, как показано ниже:
class Circle {
double radius = 0;
public Circle(double radius) {
this.radius = radius;
}
class Draw { //内部类
public void drawSahpe() {
System.out.println("drawshape");
}
}
}
Таким образом, класс Draw выглядит как член класса Circle, а Circle называется внешним классом. Внутренний класс-член может безоговорочно обращаться ко всем свойствам-членам и методам-членам внешнего класса, включая private-члены и static-члены.
class Circle {
private double radius = 0;
public static int count =1;
public Circle(double radius) {
this.radius = radius;
}
class Draw { //内部类
public void drawSahpe() {
System.out.println(radius); //外部类的private成员
System.out.println(count); //外部类的静态成员
}
}
}
Однако обратите внимание: когда внутренний класс-член имеет переменную-член или метод с тем же именем, что и внешний класс, происходит сокрытие. То есть по умолчанию осуществляется доступ к члену внутреннего класса-члена. Чтобы обратиться к одноимённому члену внешнего класса, нужно обращаться к нему в следующей форме:
外部类.this.成员变量 外部类.this.成员方法
Хотя внутренний класс-член может безоговорочно обращаться к членам внешнего класса, внешний класс не может так свободно обращаться к членам внутреннего класса-члена. Во внешнем классе, чтобы обратиться к членам внутреннего класса-члена, необходимо сначала создать объект внутреннего класса-члена, а затем обратиться к нему через ссылку, указывающую на этот объект:
class Circle {
private double radius = 0;
public Circle(double radius) {
this.radius = radius;
getDrawInstance().drawSahpe(); //必须先创建成员内部类的对象,再进行访问
}
private Draw getDrawInstance() {
return new Draw();
}
class Draw { //内部类
public void drawSahpe() {
System.out.println(radius); //外部类的private成员
}
}
}Внутренний класс-член существует привязанным к внешнему классу. То есть, чтобы создать объект внутреннего класса-члена, сначала должен существовать объект внешнего класса. Общий способ создания объекта внутреннего класса-члена выглядит так:
public class Test {
public static void main(String[] args) {
//第一种方式:
Outter outter = new Outter();
Outter.Inner inner = outter.new Inner(); //必须通过Outter对象来创建
//第二种方式:
Outter.Inner inner1 = outter.getInnerInstance();
}
}
class Outter {
private Inner inner = null;
public Outter() {
}
public Inner getInnerInstance() {
if(inner == null)
inner = new Inner();
return inner;
}
class Inner {
public Inner() {
}
}
}
Внутренние классы могут иметь доступ private, protected, public и package. Например, в приведённом выше примере, если внутренний класс-член Inner объявлен с модификатором private, к нему можно обращаться только внутри внешнего класса; если с public — к нему можно обращаться где угодно; если с protected — к нему можно обращаться только внутри того же пакета или из классов, наследующих внешний класс; если у него доступ по умолчанию, к нему можно обращаться только внутри того же пакета. Это немного отличается от внешнего класса, который можно объявлять только с public и package-доступом. Лично я понимаю это так: поскольку внутренний класс-член выглядит как член внешнего класса, он может иметь несколько модификаторов доступа, как и члены класса.
2. Локальный внутренний класс
Локальный внутренний класс — это класс, определённый внутри метода или области видимости. Его отличие от внутреннего класса-члена состоит в том, что доступ к локальному внутреннему классу ограничен этим методом или областью видимости.
class People{
public People() {
}
}
class Man{
public Man(){
}
public People getWoman(){
class Woman extends People{ //局部内部类
int age =0;
}
return new Woman();
}
} Примечание: Локальный внутренний класс похож на локальную переменную внутри метода; он не может иметь модификаторы public, protected, private или static.
3. Анонимный внутренний класс
Анонимные внутренние классы, вероятно, чаще всего используются при написании кода в повседневной работе. При написании кода слушателя событий использование анонимных внутренних классов не только удобно, но и делает код проще в сопровождении. Следующий код — это код слушателя событий Android:
scan_bt.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
}
});
history_bt.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
}
});Этот код устанавливает слушателей для двух кнопок, и здесь используются анонимные внутренние классы. В этом коде:
new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
}
}Это использование анонимного внутреннего класса. В коде необходимо установить объект слушателя для кнопки. Использование анонимного внутреннего класса позволяет создать соответствующий объект при реализации методов родительского класса или интерфейса, но обязательное условие — родительский класс или интерфейс уже должен существовать. Конечно, следующий способ также допустим и даёт тот же эффект, что и использование анонимного внутреннего класса выше.
private void setListener()
{
scan_bt.setOnClickListener(new Listener1());
history_bt.setOnClickListener(new Listener2());
}
class Listener1 implements View.OnClickListener{
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
}
}
class Listener2 implements View.OnClickListener{
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
}
}
Хотя этот способ может дать тот же эффект, он и многословен, и сложен в сопровождении, поэтому обычно для написания кода слушателя событий используются анонимные внутренние классы. Аналогично, анонимные внутренние классы не могут иметь модификаторы доступа или модификатор static.
Анонимный внутренний класс — единственный вид класса, у которого нет конструктора. Поскольку у него нет конструктора, область применения анонимных внутренних классов очень ограничена. Большинство анонимных внутренних классов используются для обратных вызовов интерфейсов. Во время компиляции система автоматически называет анонимный внутренний класс Outter$1.class. Вообще говоря, анонимные внутренние классы используются для наследования других классов или реализации интерфейсов без необходимости добавлять дополнительные методы; они только реализуют или переопределяют унаследованные методы.
4. Статический внутренний класс
Статический внутренний класс — это тоже класс, определённый внутри другого класса, только перед классом добавляется ключевое слово static. Статический внутренний класс не должен зависеть от внешнего класса, что в некоторой степени похоже на статические свойства-члены класса, и он не может использовать нестатические переменные-члены или методы внешнего класса. Это легко понять: поскольку объект статического внутреннего класса можно создать без объекта внешнего класса, если бы ему было разрешено обращаться к нестатическим членам внешнего класса, возникло бы противоречие, так как нестатические члены внешнего класса должны зависеть от конкретного объекта.
public class Test {
public static void main(String[] args) {
Outter.Inner inner = new Outter.Inner();
}
}
class Outter {
public Outter() {
}
static class Inner {
public Inner() {
}
}
}

II. Углублённое понимание внутренних классов
1. Почему внутренний класс-член может безоговорочно обращаться к членам внешнего класса?
Ранее мы уже обсуждали, что внутренний класс-член может безоговорочно обращаться к членам внешнего класса. Так как же именно это реализовано? Давайте декомпилируем файл байт-кода, чтобы выяснить это. На самом деле, когда компилятор выполняет компиляцию, он компилирует внутренний класс-член в отдельный файл байт-кода. Ниже приведён код Outter.java:
public class Outter {
private Inner inner = null;
public Outter() {
}
public Inner getInnerInstance() {
if(inner == null)
inner = new Inner();
return inner;
}
protected class Inner {
public Inner() {
}
}
}После компиляции появляются два файла байт-кода:

Декомпиляция файла Outter$Inner.class даёт следующую информацию:
E:\Workspace\Test\bin\com\cxh\test2>javap -v Outter$Inner
Compiled from "Outter.java"
public class com.cxh.test2.Outter$Inner extends java.lang.Object
SourceFile: "Outter.java"
InnerClass:
#24= #1 of #22; //Inner=class com/cxh/test2/Outter$Inner of class com/cxh/tes
t2/Outter
minor version: 0
major version: 50
Constant pool:
const #1 = class #2; // com/cxh/test2/Outter$Inner
const #2 = Asciz com/cxh/test2/Outter$Inner;
const #3 = class #4; // java/lang/Object
const #4 = Asciz java/lang/Object;
const #5 = Asciz this$0;
const #6 = Asciz Lcom/cxh/test2/Outter;;
const #7 = Asciz <init>;
const #8 = Asciz (Lcom/cxh/test2/Outter;)V;
const #9 = Asciz Code;
const #10 = Field #1.#11; // com/cxh/test2/Outter$Inner.this$0:Lcom/cxh/t
est2/Outter;
const #11 = NameAndType #5:#6;// this$0:Lcom/cxh/test2/Outter;
const #12 = Method #3.#13; // java/lang/Object."<init>":()V
const #13 = NameAndType #7:#14;// "<init>":()V
const #14 = Asciz ()V;
const #15 = Asciz LineNumberTable;
const #16 = Asciz LocalVariableTable;
const #17 = Asciz this;
const #18 = Asciz Lcom/cxh/test2/Outter$Inner;;
const #19 = Asciz SourceFile;
const #20 = Asciz Outter.java;
const #21 = Asciz InnerClasses;
const #22 = class #23; // com/cxh/test2/Outter
const #23 = Asciz com/cxh/test2/Outter;
const #24 = Asciz Inner;
{
final com.cxh.test2.Outter this$0;
public com.cxh.test2.Outter$Inner(com.cxh.test2.Outter);
Code:
Stack=2, Locals=2, Args_size=2
0: aload_0
1: aload_1
2: putfield #10; //Field this$0:Lcom/cxh/test2/Outter;
5: aload_0
6: invokespecial #12; //Method java/lang/Object."<init>":()V
9: return
LineNumberTable:
line 16: 0
line 18: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/cxh/test2/Outter$Inner;
}Строки с 11 по 35 — это содержимое пула констант. Давайте объясним содержимое в строке 38 пункт за пунктом:
final com.cxh.test2.Outter this$0;
Эта строка — указатель на объект внешнего класса. Увидев это, все, должно быть, внезапно поймут. То есть компилятор по умолчанию добавляет для внутреннего класса-члена ссылку, указывающую на объект внешнего класса. Тогда как инициализируется эта ссылка? Далее давайте посмотрим на конструктор внутреннего класса:
public com.cxh.test2.Outter$Inner(com.cxh.test2.Outter);
Из этого видно, что хотя конструктор определённого нами внутреннего класса является конструктором без аргументов, компилятор всё равно по умолчанию добавляет параметр, тип которого — ссылка на объект внешнего класса. Следовательно, указатель Outter this&0 во внутреннем классе-члене указывает на объект внешнего класса, что позволяет свободно обращаться к членам внешнего класса во внутреннем классе-члене. Это также косвенно показывает, что внутренний класс-член зависит от внешнего класса. Если объект внешнего класса не создан, ссылка Outter this&0 не может быть инициализирована и присвоена, поэтому объект внутреннего класса-члена не может быть создан.
2. Почему локальные внутренние классы и анонимные внутренние классы могут обращаться только к локальным final-переменным?
Этот вопрос, вероятно, многих озадачил. Прежде чем обсуждать его, давайте посмотрим на следующий код:
public class Test {
public static void main(String[] args) {
}
public void test(final int b) {
final int a = 10;
new Thread(){
public void run() {
System.out.println(a);
System.out.println(b);
};
}.start();
}
} Этот код будет скомпилирован в два файла классов: Test.class и Test1.class.По умолчанию компилятор называет анонимные внутренние классы и локальные внутренние классы так:Outterx.class(x — положительное целое число)。

Согласно приведённому выше рисунку, анонимный внутренний класс в методе test называется Test$1.
В приведённом выше коде, если удалить final перед любой из переменных a или b, этот код не скомпилируется. Сначала рассмотрим такую проблему:
Когда метод test завершает выполнение, жизненный цикл переменной a заканчивается. В этот момент жизненный цикл объекта Thread может ещё не завершиться. Поэтому продолжать обращаться к переменной a в методе run класса Thread становится невозможно. Но этого эффекта всё ещё нужно достичь. Что можно сделать? Java использует подход с копированием для решения этой проблемы. Декомпиляция байт-кода этого кода даёт следующее:

Мы видим, что в методе run есть инструкция:
bipush 10
Эта инструкция помещает операнд 10 в стек, указывая, что используется локальная переменная. Компилятор по умолчанию выполняет этот процесс во время компиляции. Если значение переменной можно определить во время компиляции, компилятор по умолчанию добавляет литерал с таким же содержимым в пул констант анонимного внутреннего класса (локального внутреннего класса) или напрямую встраивает соответствующий байт-код в исполняемый байт-код. Таким образом, переменная, используемая анонимным внутренним классом, — это другая локальная переменная, только её значение равно значению локальной переменной в методе, поэтому она полностью независима от локальной переменной в методе.
Теперь рассмотрим другой пример:
public class Test {
public static void main(String[] args) {
}
public void test(final int a) {
new Thread(){
public void run() {
System.out.println(a);
};
}.start();
}
}В результате декомпиляции получаем:

Мы видим, что конструктор анонимного внутреннего класса Test$1 имеет два параметра: один — ссылка на объект внешнего класса, другой — переменная типа int. Очевидно, параметр a в методе test передаётся как аргумент для инициализации копии переменной a в анонимном внутреннем классе.
То есть, если значение локальной переменной можно определить во время компиляции, копия создаётся непосредственно в анонимном внутреннем классе. Если значение локальной переменной нельзя определить во время компиляции, копия инициализируется путём передачи параметров через конструктор.
Из вышесказанного видно, что переменная a, к которой происходит доступ в методе run, вовсе не является локальной переменной a в методе test. Это решает упомянутую ранее проблему несовпадения жизненных циклов. Но возникает новая проблема: поскольку переменная a, к которой происходит доступ в методе run, и переменная a в методе test — не одна и та же переменная, что произойдёт, если значение переменной a изменить в методе run?
Да, это привело бы к несогласованности данных, что не достигло бы первоначального замысла и требований. Чтобы решить эту проблему, компилятор Java ограничивает переменную a тем, что она должна быть final-переменной, и не позволяет изменять переменную a. Для переменных ссылочного типа не разрешается указывать на новый объект. Это решает проблему несогласованности данных.
Теперь всем должно быть ясно, почему локальные переменные и параметры в методе должны быть ограничены с помощью final.
3. Есть ли что-то особенное у статических внутренних классов?
Из вышесказанного мы можем понять, что статические внутренние классы не зависят от внешнего класса, то есть можно создать объект внутреннего класса, не создавая объект внешнего класса. Кроме того, статические внутренние классы не хранят ссылку на объект внешнего класса. Читатели могут сами попробовать декомпилировать файл класса, чтобы увидеть, что там нетOutter this&0ссылки.
III. Варианты использования и преимущества внутренних классов
Зачем в Java нужны внутренние классы? В итоге, в основном есть следующие четыре пункта:
- 1. Каждый внутренний класс может независимо наследовать реализацию интерфейса, поэтому то, наследует ли внешний класс уже некоторую реализацию (интерфейса), не влияет на внутренний класс. Внутренние классы делают решение для множественного наследования полным.
- 2. Удобно организовывать классы с определёнными логическими связями вместе, а также скрывать их от внешнего мира.
- 3. Удобно для написания событийно-ориентированных программ.
- 4. Удобно для написания кода потоков.
Лично я считаю, что первый пункт — одна из самых важных причин. Существование внутренних классов делает механизм множественного наследования в Java более полным.
IV. Распространённые вопросы письменных тестов и собеседований, связанные с внутренними классами
1. Заполните код в (1), (2), (3) согласно комментариям
public class Test{
public static void main(String[] args){
// 初始化Bean1
(1)
bean1.I++;
// 初始化Bean2
(2)
bean2.J++;
//初始化Bean3
(3)
bean3.k++;
}
class Bean1{
public int I = 0;
}
static class Bean2{
public int J = 0;
}
}
class Bean{
class Bean3{
public int k = 0;
}
}
Из вышесказанного: для внутренних классов-членов сначала нужно создать экземпляр внешнего класса, прежде чем создавать экземпляр внутреннего класса. Однако для статических внутренних классов можно создать экземпляр внутреннего класса, не создавая экземпляр внешнего класса.
Общая форма создания объекта статического внутреннего класса:OuterClass.InnerClass xxx = new OuterClass.InnerClass()
Общая форма создания объекта внутреннего класса-члена:OuterClass.InnerClass xxx = outerObjectName.new InnerClass()
Следовательно, код в (1), (2), (3) соответственно:
Test test = new Test(); Test.Bean1 bean1 = test.new Bean1();
Test.Bean2 b2 = new Test.Bean2();
Bean bean = new Bean(); Bean.Bean3 bean3 = bean.new Bean3();
2. Каков вывод следующего кода?
public class Test {
public static void main(String[] args) {
Outter outter = new Outter();
outter.new Inner().print();
}
}
class Outter
{
private int a = 1;
class Inner {
private int a = 2;
public void print() {
int a = 3;
System.out.println("局部变量:" + a);
System.out.println("内部类变量:" + this.a);
System.out.println("外部类变量:" + Outter.this.a);
}
}
}3 2 1
И последнее замечание: о наследовании внутренних классов-членов. Вообще говоря, внутренние классы редко используются для наследования. Но когда их используют для наследования, нужно учесть два момента:
- 1) Ссылка на внутренний класс-член должна быть в форме Outter.Inner
- 2) Конструктор должен иметь ссылку на объект внешнего класса и вызывать super() через эту ссылку. Этот код взят из «Thinking in Java»
class WithInner {
class Inner{
}
}
class InheritInner extends WithInner.Inner {
// InheritInner() 是不能通过编译的,一定要加上形参
InheritInner(WithInner wi) {
wi.super(); //必须有这句调用
}
public static void main(String[] args) {
WithInner wi = new WithInner();
InheritInner obj = new InheritInner(wi);
}
}
Оригинальный источник: https://www.cnblogs.com/dolphin0520/p/3811445.html