Java内部类总结

什么是内部类

如果一个类定义在另一个类的内部,那么这个类就是内部类。可以通过public,protected,private修饰符来控制内部类的可见性。同时,内部类也可以被声明为abstract类型,供其他类继承和扩展。

内部类是一个编译时的概念,一旦编译成功,内部类和外部类就是两个完全不同的类。例如,对于一个外部类Outer,在其中定义一个内部类Inner,编译完成后会生成Outer.classOuter$Inner.class两个class文件。所以内部类的成员变量和方法名可以和外部类相同。

内部类用法

按照内部类的用法,可以将内部类分为成员内部类,局部内部类,匿名内部类以及静态内部类。

成员内部类

成员内部类就是在外围类的内部直接定义一个类。可以通过public,protected,private修饰符来控制其可见性。在创建内部类的时候,可以直接创建内部类,也可以让内部类继承某个基类或者接口。如下所示。

1. 直接创建内部类
public class Parcel1 {

    private class Contents {
        private String content = "goods";
        public String contents() {
            return content;
        }
    }

    public class Destination {
        private String dest;
        Destination(String dest) {
            this.dest = dest;
        }

        public String dest() {
            return dest;
        }
    }

    public void ship1(String dest) {
        Contents c = new Contents();
        Destination d = new Destination(dest);
        System.out.println("content: " + c.contents() + ", destination: " + d.dest());
    }

    public static void ship2(String dest) {
        Parcel1 p = new Parcel1();
        Contents c = p.new Contents();
        Destination d = p.new Destination("dest2");
        System.out.println("content: " + c.contents() + ", destination: " + d.dest());
    }

    public static void main(String[] args) {
        Parcel1 p = new Parcel1();
        p.ship1("dest1");

        Parcel1.ship2("dest2");
    }
}

在上面例子中,ContentsDestination都定义在Parcel1类的内部,它们都是public的,所以通过其他类也可以访问到。通过ship1方法和ship2方法,我们可以看到创建内部类对象的两种方式。

  • 在外部类的非静态方法创建内部类对象

    这种方式和使用普通类没有什么区别。可以直接通过new InnerClass()的方式来创建内部类对象。如ship1方法所示。

  • 在外部类的静态方法或者其他类中创建内部类对象

    在外部类的静态方法或者其他类创建内部类对象时,我们必须首先创建外部类对象outterObj,然后通过outterObj.new InnerClass()的方式来创建内部类对象。如ship2方法所示。

2.通过实现接口/继承基类创建内部类

除了直接创建内部类,我们还可以让内部类实现某个接口或者继承某个基类。这样,我们在使用内部类对象的时候,可以将其向上转型为对其基类或者接口的引用。这样能够方便地隐藏内部类实现细节并屏蔽类型差异。如下所示。

public interface IContents {
    String contents();
}
public interface IDestination {
    String dest();
}
public class Parcel2 {

    private class Contents implements IContents{
        private String content = "goods";

        @Override
        public String contents() {
            return content;
        }
    }

    private class Destination implements IDestination{
        private String dest;
        Destination(String dest) {
            this.dest = dest;
        }

        @Override
        public String dest() {
            return dest;
        }
    }

    public Destination destination(String dest) {
        return new Destination(dest);
    }

    public Contents contents() {
        return new Contents();
    }

    public static void main(String[] args) {
        Parcel2 p = new Parcel2();
        IContents c = p.contents();
        IDestination d = p.destination("dest1");
        System.out.println("contents: " + c.contents() + ", destination: " + d.dest());
    }
}

这里,内部类DestinationContents分别实现了接口IDestinationIContents,我们在访问内部类对象的时候,可以通过IDestinationIContents的引用来访问内部类对象,而不再需要关心内部类对象的具体类型。因此,我们可以把DestinationContents设置为private类型。设置为private类型后,在其他类中无法直接创建内部类对象,所以需要在外部类中新增destinationcontent方法来获取内部类对象。

访问外对象成员

内部类可以无缝地访问外部类对象的所有成员,因为当创建一个内部类对象的时候,内部类对象会隐式地持有一个指向外部类对象的引用,通过这个引用来访问外部类的所以成员变量及方法。当然,也可以访问外部类对象本身。关于这点,我们可以直接通过反编译内部类的class文件来验证。我们首先使用javac命令编译前面的Parcel1,编译之后,我们看到目录下生成了三个class文件。

Parcel1$1.class
Parcel1$Contents.class
Parcel1$Destination.class

这也验证了内部类是一个编译时的概念,在编译完成后,内部类和外部类就是两个完全独立的类了。通过IntelliJ IDEA我们可以查看反编译后的class文件,我们查看下Parcel1$Contents.class文件,代码如下所示。

class Parcel1$Contents {
    private String content;

    private Parcel1$Contents(Parcel1 var1) {
        this.this$0 = var1;
        this.content = "goods";
    }

    public String contents() {
        return this.content;
    }
}

可以看到,编译器为我们做了很多工作,它会自动生成一个内部类的构造方法,这个方法传入的是外部类对象的一个引用,在内部类中,然后在内部类中还会一定一个成员变量this$0来存储外部类对象的引用。所以,我们在创建一个内部类对象之前,必须首先要创建外部类对象。

持有外部类对象的引用是内部类最有用的一个特性,通过这种方式,我们可以把内部类作为访问外部类的一个窗口。所有内部类(静态内部类除外)在创建的时候都会持有外部类对象的应用,所以在创建内部类对象的时候,我们必须先创建一个外部类对象。而不能直接通过 new InnerClass的方式来创建内部类对象。例如,下面通过内部类来实现一个迭代器。

public interface Selector {
    boolean end();
    Object current();
    void next();
}
public class Sequence {

    private Object[] items;
    private int next = 0;

    public Sequence(int size) {
        items = new Object[size];
    }

    public void add(Object x) {
        if (next<items.length) {
            items[next++] = x;
        }
    }

    private class SequenceSelector implements Selector {

        private int i=0;

        @Override
        public boolean end() {
            return i==items.length;
        }

        @Override
        public Object current() {
            return items[i];
        }

        @Override
        public void next() {
            if (i < items.length) {
                i++;
            }
        }
    }

    public Selector selector() {
        return new SequenceSelector();
    }

    public static void main(String[] args) {
        Sequence sequence = new Sequence(20);
        for (int i=0;i<20;i++) {
            sequence.add(i);
        }
        Selector selector = sequence.selector();
        while (!selector.end()) {
            System.out.println(selector.current());
            selector.next();
        }
    }
}

如果想要在内部类中访问外部类对象本身,可以通过OutterClass.this来访问。

局部内部类

除了可以定义成员内部类(也就是在外部类中直接定义内部类)外,我们还可以在方法或者局部作用域内定义内部类。

1.在方法内部定义内部类
public class Parcel3 {

    public IDestination destination(String dest) {
        class Destination implements IDestination{
            private String dest;
            Destination(String dest) {
                this.dest = dest;
            }

            @Override
            public String dest() {
                return dest;
            }
        }
        return new Destination(dest);
    }

    public IContents contents() {
        class Contents implements IContents{
            private String content = "goods";

            @Override
            public String contents() {
                return content;
            }
        }
        return new Contents();
    }

    public static void main(String[] args) {
        Parcel3 p = new Parcel3();
        IContents c = p.contents();
        IDestination d = p.destination("dest1");
        System.out.println("contents: " + c.contents() + ", destination: " + d.dest());
    }
}

这里的DestinationContents类分别定义在了方法destinationcontent内部,所以他们只能在所定义的方法内部访问。方法之外的其他地方无法访问该内部类。

2.在代码块定义内部类

除了可以在方法中定义内部类外,我们也可以直接在代码块中定义内部类,如下所示。

public class Parcel4 {

    public IDestination destination(String dest) {
        class Destination implements IDestination{
            private String dest;
            Destination(String dest) {
                this.dest = dest;
            }

            @Override
            public String dest() {
                return dest;
            }
        }
        return new Destination(dest);
    }

    public IContents contents(boolean hasContent) {
         if (hasContent) {
             class Contents implements IContents {
                 private String content = "goods";

                 @Override
                 public String contents() {
                     return content;
                 }
             }
             return new Contents();
         }
         return null;
    }

    public static void main(String[] args) {
        Parcel4 p = new Parcel4();
        IContents c = p.contents(true);
        IDestination d = p.destination("dest1");
        System.out.println("content: " + c.contents() + ", destination: " + d.dest());
    }
}

可以看到Contents的定义放在了if语句内部,这就意味着,在if语句外,我们无法使用该内部类。

虽然Contents定义在了if代码块内部,但是这并不代表该类的创建是有条件的,因为内部类是在编译器生成的,编译完成后,该类和外部类没有区别,只是作用域不同罢了。

局部内部类除了作用域不同外,具有和成员内部类一样的特性,局部内部类也可以实现某个接口或者继承某个基类,也可以直接访问外部类对象中的所有成员和方法。但是局部内部类不能有访问说明符,因为他只能在方法或者代码块内部访问。

访问本地变量

当局部内部类引用本地变量时,本地变量必须是final类型或者实际上是final类型。看下面的例子。

public class Parcel7 {

    public IDestination destination(String localDest) {
        class Destination implements IDestination{
            @Override
            public String dest() {
                return localDest;
            }
        }
        return new Destination();
    }

    public IContents contents(boolean hasContent) {
        if (hasContent) {
            class Contents implements IContents {
                private String content = "goods";

                @Override
                public String contents() {
                    return content;
                }
            }
            return new Contents();
        }
        return null;
    }

    public static void main(String[] args) {
        Parcel7 p = new Parcel7();
        IContents c = p.contents(true);
        IDestination d = p.destination("dest1");
        System.out.println("content: " + c.contents() + ", destination: " + d.dest());
    }
}

在上面的例子中,在Destination是一个局部内部类,并且在Destinationdest方法中直接引用了外部类destination方法中传入的localDest参数。这Java8之前,这段代码在编译时会直接报错,报错提示如下所示(用Java7编译)。

java: 从内部类中访问本地变量localDest; 需要被声明为最终类型

也就是说localDest必须声明为final类型,为什么要声明为final类型呢?原因是本地变量的生命周期在方法执行完成的时候就结束了,而内部类的生命周期大多数啊情况下会比本地变量的生命周期要长。如果本地变量被回收,那么在内部类执行到访问本地变量的代码时就会导致空指针问题。所以,为了解决这个问题,在创建内部类的时候,会在内部类中以成员变量的形式对本地变量做一个备份,这个是编译器自动帮我们完成的。关于这点,我们可以通过反编译Parcel7类来找到答案。

我们通过javac命令编译Parcel7类,编译完成后会产生如下三个.class文件。

Parcel7$1Contents.class
Parcel7$1Destination.class
Parcel7.class

然后使用javap命令对Parcel7$1Destination.class文件进行反编译。

javap -private Parcel7$1Destination.class

反编译结果如下所示。

class Parcel7$1Destination implements IDestination {
  final java.lang.String val$localDest;
  final Parcel7 this$0;
  Parcel7$1Destination();
  public java.lang.String dest();
}

可以看到,在内部类中多了一个val$localDest的成员变量,这个成员变量就是内部类Destination中用来对Parcel7.destination方法中传入的localDest做备份的。同时也可以看到val$localDest是final类型的。这意味着,如果我们想在内部类内对localDest做修改,也是不允许的。那为什么本地变量要定义成final类型呢,先来看看final类型的特点。如果一个变量被声明为final类型,它有如下特点。

  1. 当final修饰基本数据类型的变量时,这个变量在初始化时就会被赋值,并且初始化完成之后其值就不能再被改变了。
  2. 当final修饰引用类型的变量时,这个变量在初始化时就会被赋值,并且初始化完成之后其值就不能再被改变,但是该变量指向对象的堆内存的值是可以改变的。

如果当内部类引用本地变量的时候,不把本地变量声明为final类型,在内部类或者外部类的方法中对本地变量做修改后,很容易造成理解上的混淆和代码行为的错乱,所以为了避免混淆和错乱的问题,在内部类引用本地变量的时候,需要把本地变量声明为final类型。这样内部类和方法中的本地变量值就可以始终保持一致。

在Java8之后,不再需要强制将本地变量声明为final类型,所以上面的代码在Java8下编译的时候,不会报错也可以正常执行。但是,如果我们想尝试改变本地变量的值,编译器还是会报错。例如,我们将destination做如下修改。

public IDestination destination(String localDest) {
    class Destination implements IDestination{
        @Override
        public String dest() {
            return localDest;
        }
    }

    localDest = "dest2";
    System.out.println(localDest);
    return new Destination();
}

我们在方法内重新对localDest变量进行赋值,然后编译,编译器报错如下所示。

java: 从内部类引用的本地变量必须是最终变量或实际上的最终变量

所以,Java8虽然不再需要将本地变量强制声明为final类型,但是实际上还是要求本地变量时final类型。关于这点,我们可以查看Parcel7.class文件。

public class Parcel7 {
    public Parcel7() {
    }

    public IDestination destination(final String var1) {
        class Destination implements IDestination {
            Destination() {
            }

            public String dest() {
                return var1;
            }
        }

        return new Destination();
    }
    ……
}

可以看到,如果在内部类引用了本地变量,编译器会自动将该本地变量转变为final类型。

匿名内部类

匿名内部类本质也属于一种局部内部类,它主要是用于某个类只需要创建一个对象,而不需要再使用的场景。。如下所示。

public class Parcel5 {

    public IDestination destination(String dest) {
        class Destination implements IDestination{
            private String dest;
            Destination(String dest) {
                this.dest = dest;
            }

            @Override
            public String dest() {
                return dest;
            }
        }
        return new Destination(dest);
    }

    public IContents contents() {
        return new IContents() {
            private String content = "goods";
            @Override
            public String contents() {
                return content;
            }
        };
    }

    public static void main(String[] args) {
        Parcel5 p = new Parcel5();
        IContents c = p.contents();
        IDestination d = p.destination("dest1");
        System.out.println("content: " + c.contents() + ", destination: " + d.dest());
    }
}

这里在创建Contents对象的时候,就使用到了匿名内部类。之所以叫匿名内部类,就是这个类没有名字,而是直接通过实现某个接口或者基类来创建一个对象,匿名内部类的语法格式如下所示。

new SuperType(construction parameters){
  //inner class methods and data
}

其中SuperType可以是像IContents这样的接口,也可以是一个类。在Java中定义回调函数的时候,经常会用到匿名内部类。例如,在创建一个子线程的时候,我们通常会给Thread的构造方法传递一个实现了Runnable接口的匿名内部类对象。

new Thread(new Runnable(){

  @Override
  public void run() {
    System.out.println("Thread");
  }
}).start();

上面的contents方法通过实现IContents接口创建了匿名内部类对象,默认使用了匿名内部类的无参构造器。在创建匿名内部类对象的时候,也可以使用有参数的构造器上,不过因为匿名内部类是没有名字的,所以肯定不能在匿名内部类里边定义带参数的构造器。所以,如果想在创建匿名内部类的时候,使用带参数的构造器,必须要匿名内部类的基类含有带参数的构造器。如下所示。

public class BaseDestination {
    private String dest  = "";
    public BaseDestination (String dest) {
        this.dest = dest;
    }

    public String dest() {
        return dest;
    }
}
public class Parcel6 {

    public BaseDestination destination(String dest) {
        return new BaseDestination(dest) {
            @Override
            public String dest() {
                return super.dest();
            }
        };
    }

    public IContents contents() {
        return new IContents() {
            private String content = "goods";
            @Override
            public String contents() {
                return content;
            }
        };
    }

    public static void main(String[] args) {
        Parcel6 p = new Parcel6();
        IContents c = p.contents();
        BaseDestination d = p.destination("dest1");
        System.out.println("content: " + c.contents() + ", destination: " + d.dest());
    }
}

上面的例子中,我们通过继承BaseDestination来创建了一个匿名内部类对象,在创建的时候,我们使用了匿名内部类基类的带参构造器。

由于匿名内部类本质上也是一种局部内部类,它在访问本地变量时也需要将本地变量声明为final类型或者本地变量实际上符合final类型的特点。

静态内部类

前面学习的成员内部类,局部内部类和匿名内部类对象在创建的时候都会隐式地持有外部类对象的引用,如果不想内部类和外部类对象之间有联系,可以将内部类声明为static类型,这就是所谓的静态内部类。静态内部类对象不会持有外部类对象的引用,所以在创建静态内部类对象的时候,并不需要事先创建外部类对象。由于没有持有外部类对象的引用,所以,在静态内部类中只能访问外部类的静态成员变量和静态方法,而无法访问非静态成员变量和非静态方法。

静态内部类由于切断了和外部类的联系,所以它和外部类更加独立。例如,我们在设计模式中经常用到的构建者模式,就是静态内部类的一种典型应用。如下所示。

public class Student {
    private String name = "";
    private int age;
    private String address = "";

    private Student(Builder builder) {
        name = builder.name;
        age = builder.age;
        address = builder.address;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public String getAddress() {
        return address;
    }

    public static Builder newBuilder(){
        return new Builder();
    }

    public static class Builder {
        private String name = "";
        private int age;
        private String address = "";

        public Builder setName(String name) {
            this.name = name;
            return this;
        }

        public Builder setAge(int age) {
            this.age = age;
            return this;
        }

        public Builder setAddress(String address) {
            this.address = address;
            return this;
        }

        public Student build() {
            return new Student(this);
        }
    }
}
public class BuilderTest {
    public static void main(String[] args) {
        Student.Builder builder = Student.newBuilder();
        Student student = builder.setName("xxx")
            .setAge(18)
            .setAddress("China")
            .build();
        System.out.println("name: " + student.getName() + ", age: " + student.getAge() + ", address: " +student.getAddress() );
    }
}

为什么需要内部类

那么Java为什么要使用内部类呢?通过内部类的特点和用法,我们可以总结出以下原因。

  1. 内部类定义在一个类的内部,通过内部类我们可以访问外部类的变量和方法,对外部类对象进行操作,作为访问外部类的一个窗口。
  2. 内部类可以对其他类隐藏可见性和实现细节,保证内部类的扩展性和隔离性。
  3. 内部类是实现多继承的一种方式,每个内部类都能独立的继承一个类或者实现若干个接口,而不受外围类和其他内部类的限制,所以是实现多继承的一种方案。
  4. 内部类有一些有用的特性,例如,当想要定义一个回调方法的时候,使用匿名内部类能提供很大的便捷性。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容

  • 概念介绍 内部类在Java中,可以将一个类定义在另一个类里面或者一个方法里面,这样的类称为内部类。内部类是一种非常...
    niaoge2016阅读 653评论 0 1
  • 一、内部类有几种? 内部类分为成员内部类、静态嵌套类、方法内部类、匿名内部类。几种内部类的共性:A、内部类仍然是一...
    yekai阅读 287评论 0 0
  • 前言 随着工作经验的增加,越发感觉到基础的重要性,所以最近上下班地铁上都在看一些基础的东西,前几天看了拭心大哥讲的...
    实例波阅读 507评论 0 1
  • 内部类分为四类:成员内部类、局部(作用域)内部类、匿名内部类、静态内部类。 内部类大比拼 成员内部类 就像一个成员...
    果女郎阅读 388评论 0 1
  • 前言 这几天趁着时间多多,回顾并总结出来超全面的Java内部类知识;Java内部类老实说我们在开发的时候用的不多,...
    JTravler阅读 742评论 0 1