旗下导航:搜·么
当前位置:网站首页 > JAVA教程 > 正文

java内部类详解(附相干面试题)【JAVA教程】,java

作者:搜教程发布时间:2019-12-21分类:JAVA教程浏览:59评论:0


导读:提及内部类这个词,想必很多人都不生疏,然则又会以为不熟悉。原因是日常平凡编写代码时大概用到的场景不多,用得最多的是在有事宜监听的情况下,而且纵然用到也很少去总结内部类的用法...

提及内部类这个词,想必很多人都不生疏,然则又会以为不熟悉。原因是日常平凡编写代码时大概用到的场景不多,用得最多的是在有事宜监听的情况下,而且纵然用到也很少去总结内部类的用法。本日我们就来一探终究。

一.内部类基本

在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成员和静态成员)。

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接见权限及包接见权限。比方上面的例子,假如成员内部类Inner用private润饰,则只能在外部类的内部接见,假如用public润饰,则任何处所都能接见;

假如用protected润饰,则只能在同一个包下或许继承外部类的情况下接见;假如是默许接见权限,则只能在同一个包下接见。这一点和外部类有一点不一样,外部类只能被public和包接见两种权限润饰。

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。静态内部类是不须要依赖于外部类的,这点和类的静态成员属性有点相似,而且它不能运用外部类的非static成员变量或许要领,这点很好明白,由于在没有外部类的对象的情况下,可以建立静态内部类的对象,假如许可接见外部类的非static成员就会发生矛盾,由于外部类的非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() {
             
        }
    }
}

二.深切明白内部类

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();
    }
}

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

依据上图可知,test要领中的匿名内部类的名字被起为 Test$1。

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

当test要领实行终了以后,变量a的生命周期就完毕了,而此时Thread对象的生命周期极大概还没有完毕,那末在Thread的run要领中继承接见变量a就变成不大概了,然则又要完成如许的效果,怎么办呢?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型变量,很显然,这里是将变量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内部类详解(附相干面试题)的细致内容,更多请关注ki4网别的相干文章!

标签:java


欢迎 发表评论: