> Java > Java베이스 > 본문

Java 내부 클래스에 대한 자세한 설명(관련 인터뷰 질문 포함)

풀어 주다: 2019-12-20 17:29:07
앞으로
2146명이 탐색했습니다.

Java 내부 클래스에 대한 자세한 설명(관련 인터뷰 질문 포함)

이너클래스라는 용어를 이야기하면 많은 분들이 익숙하시겠지만 낯설게 느끼실 수도 있습니다. 그 이유는 코드를 작성할 때 사용할 수 있는 시나리오가 많지 않기 때문입니다. 가장 일반적으로 사용되는 시나리오는 이벤트 모니터링이 있는 경우이며, 사용하더라도 내부 클래스의 사용법이 요약되는 경우는 거의 없습니다. 오늘 알아보겠습니다.

1. 내부 클래스의 기본

자바에서는 클래스를 다른 클래스나 메소드에서 정의할 수 있습니다. 이러한 클래스를 내부 클래스라고 합니다. 넓은 의미의 내부 클래스에는 일반적으로 멤버 내부 클래스, 로컬 내부 클래스, 익명 내부 클래스 및 정적 내부 클래스의 네 가지 유형이 포함됩니다. 먼저 이 네 가지 내부 클래스의 사용법을 이해해 보겠습니다.

1. 멤버 내부 클래스

멤버 내부 클래스는 가장 일반적인 내부 클래스로, 다음과 같은 형식으로 다른 클래스 내부에 위치한다고 정의됩니다.

class Circle {
    double radius = 0;
     
    public Circle(double radius) {
        this.radius = radius;
    }
     
    class Draw {     //内部类
        public void drawSahpe() {
            System.out.println("drawshape");
        }
    }
}
로그인 후 복사

클래스 Draw는 클래스 A 멤버와 같은 것 같습니다. Circle 중 Circle을 외부 클래스라고 합니다. 멤버 내부 클래스는 외부 클래스의 모든 멤버 속성과 멤버 메서드(전용 멤버 및 정적 멤버 포함)에 무조건 액세스할 수 있습니다.

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);   //外部类的静态成员
        }
    }
}
로그인 후 복사

그러나 멤버 내부 클래스에 외부 클래스와 이름이 같은 멤버 변수나 메서드가 있는 경우 숨겨진 현상이 발생합니다. 즉, 기본적으로 멤버 내부 클래스의 멤버에 액세스한다는 점에 유의해야 합니다. . 동일한 이름을 가진 외부 클래스의 멤버에 액세스하려면 다음 형식으로 액세스해야 합니다.

External class.this.Member 변수

External class.this.Member 메서드

멤버 내부 클래스는 무조건 외부 클래스 멤버에 접근할 수 있지만, 외부 클래스는 내부 클래스의 멤버에 그렇게 자유롭게 접근할 수 없습니다. 외부 클래스에서 멤버 내부 클래스의 멤버에 액세스하려면 먼저 멤버 내부 클래스의 개체를 만든 다음 이 개체를 가리키는 참조를 통해 액세스해야 합니다.

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() {
             
        }
    }
}
로그인 후 복사

내부 클래스는 개인 접근 권한, 보호 접근 권한, 공용 접근 권한, 패키지 접근 권한을 가질 수 있습니다. 예를 들어 위의 예에서 멤버 내부 클래스 Inner를 private으로 수정하면 외부 클래스 내부에서만 액세스할 수 있습니다. public으로 수정하면

protect로 수정하면 어디서나 액세스할 수 있습니다. , 동일한 패키지 내에서만 액세스할 수 있으며, 외부 클래스를 상속하는 경우에는 동일한 패키지에서만 액세스할 수 있습니다. 이는 외부 클래스와 약간 다릅니다. 외부 클래스는 공용 및 패키지 액세스 권한으로만 수정할 수 있습니다.

2. 로컬 내부 클래스

로컬 내부 클래스는 메서드나 범위에 정의된 클래스입니다. 멤버 내부 클래스와 차이점은 로컬 내부 클래스의 액세스가 메서드 또는 범위로 제한된다는 것입니다. 범위.

class People{
    public People() {
         
    }
}
 
class Man{
    public Man(){
         
    }
     
    public People getWoman(){
        class Woman extends People{   //局部内部类
            int age =0;
        }
        return new Woman();
    }
}
로그인 후 복사

로컬 내부 클래스는 메서드의 로컬 변수와 같으며 공개, 보호, 비공개 및 정적 수정자를 가질 수 없습니다.

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
             
    }
}
로그인 후 복사

이 작성 방법은 동일한 효과를 얻을 수 있지만 시간이 오래 걸리고 유지 관리가 어렵기 때문에 일반적으로 이벤트 모니터링 코드를 작성하는 데 익명 내부 클래스 방법이 사용됩니다. 마찬가지로 익명 내부 클래스는 액세스 한정자와 정적 한정자를 가질 수 없습니다.

익명 내부 클래스는 생성자가 없는 유일한 클래스입니다. 생성자가 없기 때문에 익명 내부 클래스의 사용 범위가 매우 제한됩니다. 대부분의 익명 내부 클래스는 인터페이스 콜백에 사용됩니다. 익명 내부 클래스는 컴파일 중에 시스템에 의해 자동으로 Oututter$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() {
             
        }
    }
}
로그인 후 복사

Java 내부 클래스에 대한 자세한 설명(관련 인터뷰 질문 포함)

二.深入理解内部类

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() {
             
        }
    }
}
로그인 후 복사

编译之后,出现了两个字节码文件:

Java 내부 클래스에 대한 자세한 설명(관련 인터뷰 질문 포함)

反编译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();
    }
}
로그인 후 복사

这段代码会被编译成两个class文件:Test.class和Test1.class。默认情况下,编译器会为匿名内部类和局部内部类起名为Outter1.class。默认情况下,编译器会为匿名内部类和局部内部类起名为Outterx.class(x为正整数)。

Java 내부 클래스에 대한 자세한 설명(관련 인터뷰 질문 포함)

根据上图可知,test方法中的匿名内部类的名字被起为 Test$1。

上段代码中,如果把变量a和b前面的任一个final去掉,这段代码都编译不过。我们先考虑这样一个问题:

当test方法执行完毕之后,变量a的生命周期就结束了,而此时Thread对象的生命周期很可能还没有结束,那么在Thread的run方法中继续访问变量a就变成不可能了,但是又要实现这样的效果,怎么办呢?Java采用了 复制 的手段来解决这个问题。将这段代码的字节码反编译可以得到下面的内容:

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();
    }
}
로그인 후 복사

反编译得到:

Java 내부 클래스에 대한 자세한 설명(관련 인터뷰 질문 포함)

我们看到匿名内部类Test$1的构造器含有两个参数,一个是指向外部类对象的引用,一个是int型变量,很显然,这里是将变量test方法中的形参a以参数的形式传进来对匿名内部类中的拷贝(变量a的拷贝)进行赋值初始化。

也就说如果局部变量的值在编译期间就可以确定,则直接在匿名内部里面创建一个拷贝。如果局部变量的值无法在编译期间确定,则通过构造器传参的方式来对拷贝进行初始化赋值。

从上面可以看出,在run方法中访问的变量a根本就不是test方法中的局部变量a。这样一来就解决了前面所说的 生命周期不一致的问题。

但是新的问题又来了,既然在run方法中访问的变量a和test方法中的变量a不是同一个变量,当在run方法中改变变量a的值的话,会出现什么情况?

对,会造成数据不一致性,这样就达不到原本的意图和要求。为了解决这个问题,java编译器就限定必须将变量a限制为final变量,不允许对变量a进行更改(对于引用类型的变量,是不允许指向新的对象),这样数据不一致性的问题就得以解决了。

到这里,想必大家应该清楚为何 方法中的局部变量和形参都必须用final进行限定了。

3.静态内部类有特殊的地方吗?

从前面可以知道,静态内部类是不依赖于外部类的,也就说可以在不创建外部类对象的情况下创建内部类的对象。另外,静态内部类是不持有指向外部类对象的引用的,这个读者可以自己尝试反编译class文件看一下就知道了,是没有Outter this&0引用的。

三.内部类的使用场景和好处

为什么在Java中需要内部类?总结一下主要有以下四点:

1.每个内部类都能独立的继承一个接口的实现,所以无论外部类是否已经继承了某个(接口的)实现,对于内部类都没有影响。内部类使得多继承的解决方案变得完整,

2.方便将存在一定逻辑关系的类组织在一起,又可以对外界隐藏。

3.方便编写事件驱动程序

4.方便编写线程代码

四.常见的与内部类相关的笔试面试题

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;
    }
}
로그인 후 복사

从前面可知,对于成员内部类,必须先产生外部类的实例化对象,才能产生内部类的实例化对象。而静态内部类不用产生外部类的实例化对象即可产生内部类的实例化对象。

创建静态内部类对象的一般形式为: 外部类类名.内部类类名 xxx = new 外部类类名.内部类类名()

创建成员内部类对象的一般形式为: 外部类类名.内部类类名 xxx = 外部类对象名.new 内部类类名()

因此,(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()。

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);
    }
}
로그인 후 복사

更多java知识请关注java基础教程栏目。

위 내용은 Java 내부 클래스에 대한 자세한 설명(관련 인터뷰 질문 포함)의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

관련 라벨:
원천:cnblogs.com
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿