Java基础8:深刻理解内部类

更多内容请关注微信公众号【Java技术江湖】java

这是一位阿里 Java 工程师的技术小站,做者黄小斜,专一 Java 相关技术:SSM、SpringBoot、MySQL、分布式、中间件、集群、Linux、网络、多线程,偶尔讲点Docker、ELK,同时也分享技术干货和学习经验,致力于Java全栈开发!(关注公众号后回复”资料“便可领取 3T 免费技术学习资源以及我我原创的程序员校招指南、Java学习指南等资源)android

**
本文主要介绍了Java内部类的基本原理,使用方法和各类细节。git

有关内部类实现回调,事件驱动和委托机制的文章将在后面发布。程序员

具体代码在个人GitHub中能够找到github

https://github.com/h2pl/MyTech编程

文章首发于个人我的博客:微信

https://h2pl.github.io/2018/0...网络

内部类初探

1、什么是内部类?多线程

  内部类是指在一个外部类的内部再定义一个类。内部类做为外部类的一个成员,而且依附于外部类而存在的。内部类可为静态,可用protected和private修饰(而外部类只能使用public和缺省的包访问权限)。内部类主要有如下几类:成员内部类、局部内部类、静态内部类、匿名内部类分布式

2、内部类的共性

(1)内部类仍然是一个独立的类,在编译以后内部类会被编译成独立的.class文件,可是前面冠之外部类的类名和$符号 。

(2)内部类不能用普通的方式访问。

(3)内部类声明成静态的,就不能随便的访问外部类的成员变量了,此时内部类只能访问外部类的静态成员变量 。

(4)外部类不能直接访问内部类的的成员,但能够经过内部类对象来访问

  内部类是外部类的一个成员,所以内部类能够自由地访问外部类的成员变量,不管是不是private的。

  由于当某个外围类的对象建立内部类的对象时,此内部类会捕获一个隐式引用,它引用了实例化该内部对象的外围类对象。经过这个指针,能够访问外围类对象的所有状态。

经过反编译内部类的字节码,分析以后主要是经过如下几步作到的:

  1 编译器自动为内部类添加一个成员变量, 这个成员变量的类型和外部类的类型相同, 这个成员变量就是指向外部类对象的引用;

  2 编译器自动为内部类的构造方法添加一个参数, 参数的类型是外部类的类型, 在构造方法内部使用这个参数为1中添加的成员变量赋值;

  3 在调用内部类的构造函数初始化内部类对象时, 会默认传入外部类的引用。

2、使用内部类的好处:

静态内部类的做用:

1 只是为了下降包的深度,方便类的使用,静态内部类适用于包含类当中,但又不依赖与外在的类。

2 因为Java规定静态内部类不能用使用外在类的非静态属性和方法,因此只是为了方便管理类结构而定义。因而咱们在建立静态内部类的时候,不须要外部类对象的引用。

非静态内部类的做用:

1 内部类继承自某个类或实现某个接口,内部类的代码操做建立其余外围类的对象。因此你能够认为内部类提供了某种进入其外围类的窗口。

2 使用内部类最吸引人的缘由是:每一个内部类都能独立地继承自一个(接口的)实现,因此不管外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响

3 若是没有内部类提供的能够继承多个具体的或抽象的类的能力,一些设计与编程问题就很难解决。
从这个角度看,内部类使得多重继承的解决方案变得完整。接口解决了部分问题,而内部类有效地实现了"多重继承"。

3、
那静态内部类与普通内部类有什么区别呢?问得好,区别以下:

(1)静态内部类不持有外部类的引用
在普通内部类中,咱们能够直接访问外部类的属性、方法,即便是private类型也能够访问,这是由于内部类持有一个外部类的引用,能够自由访问。而静态内部类,则只能够访问外部类的静态方法和静态属性(若是是private权限也能访问,这是由其代码位置所决定的),其余则不能访问。

(2)静态内部类不依赖外部类
普通内部类与外部类之间是相互依赖的关系,内部类实例不能脱离外部类实例,也就是说它们会同生同死,一块儿声明,一块儿被垃圾回收器回收。而静态内部类是能够独立存在的,即便外部类消亡了,静态内部类仍是能够存在的。

(3)普通内部类不能声明static的方法和变量
普通内部类不能声明static的方法和变量,注意这里说的是变量,常量(也就是final static修饰的属性)仍是能够的,而静态内部类形似外部类,没有任何限制。

==为何普通内部类不能有静态变量呢?==

1 成员内部类 之因此叫作成员 就是说他是类实例的一部分 而不是类的一部分

2 结构上来讲 他和你声明的成员变量是同样的地位 一个特殊的成员变量 而静态的变量是类的一部分和实例无关

3 你若声明一个成员内部类 让他成为主类的实例一部分 而后又想在内部类声明和实例无关的静态的东西 你让JVM情何以堪啊

4 若想在内部类内声明静态字段 就必须将其内部类自己声明为静态

非静态内部类有一个很大的优势:能够自由使用外部类的全部变量和方法

下面的例子大概地介绍了

1 非静态内部类和静态内部类的区别。

2 不一样访问权限的内部类的使用。

3 外部类和它的内部类之间的关系

//本节讨论内部类以及不一样访问权限的控制
//内部类只有在使用时才会被加载。
//外部类B
public class B{
    int i = 1;
    int j = 1;
    static int s = 1;
    static int ss = 1;
    A a;
    AA aa;
    AAA aaa;
    //内部类A

    public class A {
//        static void go () {
//
//        }
//        static {
//
//        }
//      static int b = 1;//非静态内部类不能有静态成员变量和静态代码块和静态方法,
        // 由于内部类在外部类加载时并不会被加载和初始化。
        //因此不会进行静态代码的调用
        int i = 2;//外部类没法读取内部类的成员,而内部类能够直接访问外部类成员

        public void test() {
            System.out.println(j);
            j = 2;
            System.out.println(j);
            System.out.println(s);//能够访问类的静态成员变量
        }
        public void test2() {
            AA aa = new AA();
            AAA aaa = new AAA();
        }

    }
    //静态内部类S,能够被外部访问
    public static class S {
        int i = 1;//访问不到非静态变量。
        static int s = 0;//能够有静态变量

        public static void main(String[] args) {
            System.out.println(s);
        }
        @Test
        public void test () {
//            System.out.println(j);//报错,静态内部类不能读取外部类的非静态变量
            System.out.println(s);
            System.out.println(ss);
            s = 2;
            ss = 2;
            System.out.println(s);
            System.out.println(ss);
        }
    }

    //内部类AA,其实这里加protected至关于default
    //由于外部类要调用内部类只能经过B。而且没法直接继承AA,因此必须在同包
    //的类中才能调用到(这里不考虑静态内部类),那么就和default同样了。
    protected class AA{
        int i = 2;//内部类之间不共享变量
        public void test (){
            A a = new A();
            AAA aaa = new AAA();
            //内部类之间能够互相访问。
        }
    }
    //包外部依然没法访问,由于包没有继承关系,因此找不到这个类
    protected static class SS{
        int i = 2;//内部类之间不共享变量
        public void test (){

            //内部类之间能够互相访问。
        }
    }
    //私有内部类A,对外不可见,但对内部类和父类可见
    private class AAA {
        int i = 2;//内部类之间不共享变量

        public void test() {
            A a = new A();
            AA aa = new AA();
            //内部类之间能够互相访问。
        }
    }
    @Test
    public void test(){
        A a = new A();
        a.test();
        //内部类能够修改外部类的成员变量
        //打印出 1 2
        B b = new B();

    }
}


//另外一个外部类
class C {
    @Test
    public void test() {
        //首先,其余类内部类只能经过外部类来获取其实例。
        B.S s = new B.S();
        //静态内部类能够直接经过B类直接获取,不须要B的实例,和静态成员变量相似。
        //B.A a = new B.A();
        //当A不是静态类时这行代码会报错。
        //须要使用B的实例来获取A的实例
        B b = new B();
        B.A a = b.new A();
        B.AA aa = b.new AA();//B和C同包,因此能够访问到AA
//      B.AAA aaa = b.new AAA();AAA为私有内部类,外部类不可见
        //当A使用private修饰时,使用B的实例也没法获取A的实例,这一点和私有变量是同样的。
        //全部普通的内部类与类中的一个变量是相似的。静态内部类则与静态成员相似。
    }
}

内部类的加载

可能刚才的例子中没办法直观地看到内部类是如何加载的,接下来用例子展现一下内部类加载的过程。

1 内部类是延时加载的,也就是说只会在第一次使用时加载。不使用就不加载,因此能够很好的实现单例模式。

2 不管是静态内部类仍是非静态内部类都是在第一次使用时才会被加载。

3 对于非静态内部类是不能出现静态模块(包含静态块,静态属性,静态方法等)

4 非静态类的使用须要依赖于外部类的对象,详见上述对象innerClass 的初始化。

简单来讲,类的加载都是发生在类要被用到的时候。内部类也是同样

1 普通内部类在第一次用到时加载,而且每次实例化时都会执行内部成员变量的初始化,以及代码块和构造方法。

2 静态内部类也是在第一次用到时被加载。可是当它加载完之后就会将静态成员变量初始化,运行静态代码块,而且只执行一次。固然,非静态成员和代码块每次实例化时也会执行。

总结一下Java类代码加载的顺序,万变不离其宗。

规律1、初始化构造时,先父后子;只有在父类全部都构造完后子类才被初始化

规律2、类加载先是静态、后非静态、最后是构造函数。

静态构造块、静态类属性按出如今类定义里面的前后顺序初始化,同理非静态的也是同样的,只是静态的只在加载字节码时执行一次,无论你new多少次,非静态会在new多少次就执行多少次

规律3、java中的类只有在被用到的时候才会被加载

规律4、java类只有在类字节码被加载后才能够被构形成对象实例

成员内部类

在方法中定义的内部类称为局部内部类。与局部变量相似,局部内部类不能有访问说明符,由于它不是外围类的一部分,可是它能够访问当前代码块内的常量,和此外围类全部的成员。

须要注意的是:
局部内部类只能在定义该内部类的方法内实例化,不能够在此方法外对其实例化。

public class 局部内部类 {
    class A {//局部内部类就是写在方法里的类,只在方法执行时加载,一次性使用。
        public void test() {
            class B {
                public void test () {
                    class C {

                    }
                }
            }
        }
    }
    @Test
    public void test () {
        int i = 1;
        final int j = 2;
        class A {
            @Test
            public void test () {
                System.out.println(i);
                System.out.println(j);
            }
        }
        A a = new A();
        System.out.println(a);
    }

    static class B {
        public static void test () {
            //static class A报错,方法里不能定义静态内部类。
            //由于只有在方法调用时才能进行类加载和初始化。

        }
    }
}

匿名内部类

简单地说:匿名内部类就是没有名字的内部类,而且,匿名内部类是局部内部类的一种特殊形式。什么状况下须要使用匿名内部类?若是知足下面的一些条件,使用匿名内部类是比较合适的:
只用到类的一个实例。
类在定义后立刻用到。
类很是小(SUN推荐是在4行代码如下)
给类命名并不会致使你的代码更容易被理解。
在使用匿名内部类时,要记住如下几个原则:

1  匿名内部类不能有构造方法。

2  匿名内部类不能定义任何静态成员、方法和类。

3  匿名内部类不能是public,protected,private,static。

4  只能建立匿名内部类的一个实例。

5 一个匿名内部类必定是在new的后面,用其隐含实现一个接口或实现一个类。

6  因匿名内部类为局部内部类,因此局部内部类的全部限制都对其生效。

一个匿名内部类的例子:

public class 匿名内部类 {

}
interface D{
    void run ();
}
abstract class E{
    E (){

    }
    abstract void work();
}
class A {

        @Test
        public void test (int k) {
            //利用接口写出一个实现该接口的类的实例。
            //有且仅有一个实例,这个类没法重用。
            new Runnable() {
                @Override
                public void run() {
//                    k = 1;报错,当外部方法中的局部变量在内部类使用中必须改成final类型。
                    //由于方外部法中即便改变了这个变量也不会反映到内部类中。
                    //因此对于内部类来说这只是一个常量。
                    System.out.println(100);
                    System.out.println(k);
                }
            };
            new D(){
                //实现接口的匿名类
                int i =1;
                @Override
                public void run() {
                    System.out.println("run");
                    System.out.println(i);
                    System.out.println(k);
                }
            }.run();
            new E(){
                //继承抽象类的匿名类
                int i = 1;
                void run (int j) {
                    j = 1;
                }

                @Override
                void work() {

                }
            };
        }

}

匿名内部类里的final

使用的形参为什么要为final

参考文件:http://android.blog.51cto.com...

咱们给匿名内部类传递参数的时候,若该形参在内部类中须要被使用,那么该形参必需要为final。也就是说:当所在的方法的形参须要被内部类里面使用时,该形参必须为final。

为何必需要为final呢?

首先咱们知道在内部类编译成功后,它会产生一个class文件,该class文件与外部类并非同一class文件,仅仅只保留对外部类的引用。当外部类传入的参数须要被内部类调用时,从java程序的角度来看是直接被调用:

public class OuterClass {
    public void display(final String name,String age){
        class InnerClass{
            void display(){
                System.out.println(name);
            }
        }
    }
}

从上面代码中看好像name参数应该是被内部类直接调用?其实否则,在java编译以后实际的操做以下:

public class OuterClass$InnerClass {
    public InnerClass(String name,String age){
        this.InnerClass$name = name;
        this.InnerClass$age = age;
    }
    
    
    public void display(){
        System.out.println(this.InnerClass$name + "----" + this.InnerClass$age );
    }
}

因此从上面代码来看,内部类并非直接调用方法传递的参数,而是利用自身的构造器对传入的参数进行备份,本身内部方法调用的实际上时本身的属性而不是外部方法传递进来的参数。

直到这里尚未解释为何是final

在内部类中的属性和外部方法的参数二者从外表上看是同一个东西,但实际上却不是,因此他们二者是能够任意变化的,也就是说在内部类中我对属性的改变并不会影响到外部的形参,而然这从程序员的角度来看这是不可行的。

毕竟站在程序的角度来看这两个根本就是同一个,若是内部类该变了,而外部方法的形参却没有改变这是难以理解和不可接受的,因此为了保持参数的一致性,就规定使用final来避免形参的不改变。

简单理解就是,拷贝引用,为了不引用值发生改变,例如被外部类的方法修改等,而致使内部类获得的值不一致,因而用final来让该引用不可改变。

故若是定义了一个匿名内部类,而且但愿它使用一个其外部定义的参数,那么编译器会要求该参数引用是final的。

内部类初始化

咱们通常都是利用构造器来完成某个实例的初始化工做的,可是匿名内部类是没有构造器的!那怎么来初始化匿名内部类呢?使用构造代码块!利用构造代码块可以达到为匿名内部类建立一个构造器的效果。

public class OutClass {
    public InnerClass getInnerClass(final int age,final String name){
        return new InnerClass() {
            int age_ ;
            String name_;
            //构造代码块完成初始化工做
            {
                if(0 < age && age < 200){
                    age_ = age;
                    name_ = name;
                }
            }
            public String getName() {
                return name_;
            }
            
            public int getAge() {
                return age_;
            }
        };
    }

内部类的重载

  若是你建立了一个内部类,而后继承其外围类并从新定义此内部类时,会发生什么呢?也就是说,内部类能够被重载吗?这看起来彷佛是个颇有用的点子,可是“重载”内部类就好像它是外围类的一个方法,其实并不起什么做用:

class Egg {
       private Yolk y;
 
       protected class Yolk {
              public Yolk() {
                     System.out.println("Egg.Yolk()");
              }
       }
 
       public Egg() {
              System.out.println("New Egg()");
              y = new Yolk();
       }
}
 
public class BigEgg extends Egg {
       public class Yolk {
              public Yolk() {
                     System.out.println("BigEgg.Yolk()");
              }
       }
 
       public static void main(String[] args) {
              new BigEgg();
       }
}
复制代码
输出结果为:
New Egg()
Egg.Yolk()

缺省的构造器是编译器自动生成的,这里是调用基类的缺省构造器。你可能认为既然建立了BigEgg 的对象,那么所使用的应该是被“重载”过的Yolk,但你能够从输出中看到实际状况并非这样的。
这个例子说明,当你继承了某个外围类的时候,内部类并无发生什么特别神奇的变化。这两个内部类是彻底独立的两个实体,各自在本身的命名空间内。

内部类的继承

由于内部类的构造器要用到其外围类对象的引用,因此在你继承一个内部类的时候,事情变得有点复杂。问题在于,那个“秘密的”外围类对象的引用必须被初始化,而在被继承的类中并不存在要联接的缺省对象。要解决这个问题,需使用专门的语法来明确说清它们之间的关联:

class WithInner {
        class Inner {
                Inner(){
                        System.out.println("this is a constructor in WithInner.Inner");
                };
        }
}
 
public class InheritInner extends WithInner.Inner {
        // ! InheritInner() {} // Won't compile
        InheritInner(WithInner wi) {
                wi.super();
                System.out.println("this is a constructor in InheritInner");
        }
 
        public static void main(String[] args) {
                WithInner wi = new WithInner();
                InheritInner ii = new InheritInner(wi);
        }
}

复制代码
输出结果为:
this is a constructor in WithInner.Inner
this is a constructor in InheritInner

能够看到,InheritInner 只继承自内部类,而不是外围类。可是当要生成一个构造器时,缺省的构造器并不算好,并且你不能只是传递一个指向外围类对象的引用。此外,你必须在构造器内使用以下语法:
enclosingClassReference.super();
这样才提供了必要的引用,而后程序才能编译经过。

有关匿名内部类实现回调,事件驱动,委托等机制的文章将在下一节讲述。

相关文章
相关标签/搜索