面试复习之—Java基础(九):内部类

最近在准备面试,把知识点复习一遍,整理出的笔记记录下,里面会穿插代码和面试例题。

内容不是原创,是总结和收集,并在理解的基础上进行一些完善,如果侵权了请联系作者,若有错误也请各位指正。因为收集的时候忘记把来源记录下来了,所以就不po出处了,请见谅(这是个坏习惯,一定改)。

面试复习之—Java基础(九):内部类

  • 内部类
    • 内部类概述
      • 成员内部类
      • 局部内部类
      • 匿名内部类
      • 静态内部类
      • 为什么成员内部类可以自由访问外部类的成员
      • 为什么局部内部类和匿名内部类只能访问局部final变量?
    • 为什么使用内部类
      • 内部类的好处
      • 普通内部类的作用:
      • 静态内部类的作用:
    • 静态内部类与普通内部类的区别
      • 普通内部类不能声明静态变量的原因
    • Java类代码加载的顺序:
    • 内部类的加载
    • 内部类的实例化
    • 内部类的继承



这是面试复习内容的第九篇——内部类,主要是Java基础的内容,所有内容将分为几篇来写。

内部类


内部类概述

将一个类的定义放在另一个类的定义内部,这就是内部类。

内部类作为外部类的一个成员,并且依附于外部类而存在的。内部类可为静态,可用protected和private修饰(而外部类只能使用public和缺省的包访问权限)。内部类主要有以下几类:成员内部类、局部内部类、静态内部类、匿名内部类。

  1. 内部类仍然是一个独立的类,在编译之后内部类会被编译成独立的.class文件,但是前面冠以外部类的类名和$符号 。
  2. 内部类不能用普通的方式访问。外部类也不能直接访问内部类的的成员,但可以通过内部类对象来访问。
  3. 内部类是外部类的一个成员,会默认传入外部类的引用,因此内部类可以自由地访问外部类的成员变量,无论是否是private的。
  4. 内部类声明成静态的,就只能访问外部类的静态成员变量了。

成员内部类

成员内部类是最普通的内部类,它的定义为位于另一个类的内部,形如下面的形式:

class Outter {

    class Inner {     //内部类
        public void Inner() {
            System.out.println("Inner");
        }
    }
}

不过要注意的是,当成员内部类拥有和外部类同名的成员变量或者方法时,会发生隐藏现象,即默认情况下访问的是成员内部类的成员。如果要访问外部类的同名成员,需要以下面的形式进行访问:

外部类.this.成员变量
外部类.this.成员方法

虽然成员内部类可以无条件地访问外部类的成员,而外部类想访问成员内部类的成员却不是这么随心所欲了。在外部类中如果要访问成员内部类的成员,必须先创建一个成员内部类的对象,再通过指向这个对象的引用来访问:

class Outter {

    private double num = 0;

    public Outter(double num) {
        this.num = num;
        getInnerInstance().inner();   //必须先创建成员内部类的对象,再进行访问
    }

    private Inner getInnerInstance() {
        return new Inner();
    }

    class Inner {     //内部类
        public void inner() {
            System.out.println("Inner");
        }
    }
}


局部内部类

局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内。注意,局部内部类就像是方法里面的一个局部变量一样,是不能有public、protected、private以及static修饰符的。

class People{
    public People(){}
}

class Asian{
    public Asian(){}

    public People getChinese(){
        class Chinese extends People{//局部内部类
            int age = 18;
        }
        return new Chinese();
    }
}


匿名内部类

匿名内部类应该是平时我们编写代码时用得最多的,在编写事件监听的代码时使用匿名内部类不但方便,而且使代码更加容易维护。

public class Test {

    public static void main(String[] args) {

        Thread t = new Thread(new Runnable() {
            public void run() {
            }
        });
        t.start();
        
		//Java8新特性 Lambda表达式
		 new Thread(()->{System.out.println("多线程任务执行!")},"t").start();
     }
}
  1. 匿名内部类不能有构造方法。
  2. 匿名内部类不能定义任何静态成员、方法和类。
  3. 匿名内部类不能是public,protected,private,static。
  4. 只能创建匿名内部类的一个实例。
  5. 一个匿名内部类一定是在new的后面,用其隐含实现一个接口或实现一个类。
  6. 因匿名内部类为局部内部类,所以局部内部类的所有限制都对其生效。

静态内部类

静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字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() {
             
        }
    }
}


为什么成员内部类可以自由访问外部类的成员

通过反编译字节码文件看看究竟。事实上,编译器在进行编译的时候,会将成员内部类单独编译成一个字节码文件。下面是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文件得到下面信息:
面试复习之—Java基础(九):内部类_第1张图片

第11行到35行是常量池的内容,下面看看第38行的内容:

final com.inner.test2.Outter this$0;

这行是一个指向外部类对象的指针,看到这里想必大家豁然开朗了。也就是说编译器会默认为成员内部类添加了一个指向外部类对象的引用,那么这个引用是如何赋初值的呢?下面接着看内部类的构造器:

public com.inner.test2.Outter$Inner(com.inner.test2.Outter);

从这里可以看出,虽然我们在定义的内部类的构造器是无参构造器,编译器还是会默认添加一个参数,该参数的类型为指向外部类对象的一个引用,所以成员内部类中的Outter this&0 指针便指向了外部类对象,因此可以在成员内部类中随意访问外部类的成员。从这里也间接说明了成员内部类是依赖于外部类的,如果没有创建外部类的对象,则无法对Outter this&0引用进行初始化赋值,也就无法创建成员内部类的对象了。

为什么局部内部类和匿名内部类只能访问局部final变量?

当类中,或者方法中定义了一个局部变量,当匿名(局部)内部类使用该变量时(比如线程方法),变量生命周期随着外部类一起被回收,而内部类不一定结束了,Java采用了 复制 的手段来保证变量的可用,即对变量进行一份拷贝,内部类使用的变量是另一个局部变量,只不过值和方法中局部变量的值相等。

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

但是这个方式会存在数据不一致问题,意思是当源数据改变了,而拷贝的数据却还保留原值的情况。所以为了解决这个问题,java编译器就限定必须将变量限制为final变量,不允许对变量进行更改(对于引用类型的变量,是不允许指向新的对象),这样数据不一致性的问题就得以解决了。

为什么使用内部类

在《Think in java》中有这样一句话:

使用内部类最吸引人的原因是:每个内部类都能独立地继承一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响。

在我们程序设计中有时候会存在一些使用接口很难解决的问题,这个时候我们可以利用内部类提供的、可以继承多个具体的或者抽象的类的能力来解决这些程序设计问题。可以这样说,接口只是解决了部分问题,而内部类使得多重继承的解决方案变得更加完整。

内部类的好处

  • 每个内部类都能独立的继承一个接口的实现,所以无论外部类是否已经继承了某个(接口的)实现,对于内部类都没有影响。内部类使得多继承的解决方案变得完整,
  • 方便将存在一定逻辑关系的类组织在一起,又可以对外界隐藏。
  • 方便编写事件驱动程序
  • 方便编写线程代码

普通内部类的作用:

  • 内部类继承自某个类或实现某个接口,内部类的代码操作创建其他外围类的对象。所以你可以认为内部类提供了某种进入其外围类的窗口。
  • 使用内部类最吸引人的原因是:每个内部类都能独立地继承自一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响。
  • 如果没有内部类提供的可以继承多个具体的或抽象的类的能力,一些设计与编程问题就很难解决。从这个角度看,内部类使得多重继承的解决方案变得完整。接口解决了部分问题,而内部类有效地实现了"多重继承"。

静态内部类的作用:

  • 只是为了降低包的深度,方便类的使用,静态内部类适用于包含类当中,但又不依赖与外在的类。
  • 由于Java规定静态内部类不能用使用外在类的非静态属性和方法,所以只是为了方便管理类结构而定义。于是我们在创建静态内部类的时候,不需要外部类对象的引用。

静态内部类与普通内部类的区别

(1)外部类的引用的持有。

  • 普通内部类持有一个外部类的引用,可以自由访问外部类的属性、方法,private类型也可以访问。而静态内部类不持有外部类的引用,只能访问外部类的静态方法和静态属性(private属性的静态类型也可以访问),其他则不能访问。

(2)生存周期不同。

  • 普通内部类依赖于外部类,内部类实例不能脱离外部类实例,他们的生存周期相同,一起声明,一起被垃圾回收器回收。而静态内部类是可以独立存在的,即使外部类消亡了,静态内部类还可以存在。

(3)普通内部类不能声明static的方法和变量。

  • 普通内部类不能声明static的方法和变量,注意是变量,常量(也就是final
    static修饰的属性)还是可以的。而静态内部类没有限制,和外部类类似,都可以声明。


普通内部类不能声明静态变量的原因

结构上来说

  • 内部类与外部类声明的成员变量是一样的地位,可以说是一个特殊的成员变量,作为外部类的成员依赖于外部类。而静态的变量是类的一部分,和实例无关。

从JVM加载角度来说

  • java类加载顺序是,首先加载类,执行static变量初始化,接下来执行对象的创建。那么必须先执行加载外部类,再加载内部类,java虚拟机要求所有的静态变量必须在对象创建之前完成,这样便产生了矛盾。
  • :java常量放在内存中常量池,它的机制与变量是不同的,编译时,加载常量是不需要加载类的,所以就没有上面那种矛盾。

Java类代码加载的顺序:

1)先加载类,然后执行static变量初始化,接下来执行对象的创建。
2)java中的类只有在被用到的时候才会被加载。
3)java类只有在类字节码被加载后才可以被构造成对象实例。
4)初始化构造时,先父后子;只有在父类所有都构造完后子类才被初始化。
5)静态的只在加载字节码时执行一次,即在对象实例第一次new的时候执行且只执行一次;非静态new多少次就会执行多少次。


内部类的加载

内部类是延时加载的,也就是说只会在第一次使用时加载。不使用就不加载,所以可以很好的实现单例模式。不论是静态内部类还是非静态内部类都是在第一次使用时才会被加载。

  1. 普通内部类在第一次用到时加载,并且每次实例化时都会执行内部成员变量的初始化,以及代码块和构造方法。
  2. 静态内部类也是在第一次用到时被加载。但是当它加载完以后就会将静态成员变量初始化,运行静态代码块,并且只执行一次。

内部类的实例化

成员内部类是依附外部类而存在的,也就是说,如果要创建成员内部类的对象,前提是必须存在一个外部类的对象

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

        //静态内部类
        Outter.Inner2 inner2 = new Outter.Inner2();
    }
}

class Outter {
    private Inner inner = null;
    public Outter() {

    }

    public Inner getInnerInstance() {
        if(inner == null)
            inner = new Inner();
        return inner;
    }

    class Inner {
        public Inner() {

        }
    }

    static class Inner2{
        public Inner2() {

        }
    }
}


内部类的继承

关于成员内部类的继承问题。一般来说,内部类是很少用来作为继承用的。但是当用来继承的话,要注意两点:

  1. 成员内部类的引用方式必须为 Outter.Inner.
  2. 构造器中必须有指向外部类对象的引用,并通过这个引用调用super()。这段代码摘自《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);
    }
}

你可能感兴趣的:(面试,Java基础)