Saul's blog Saul's blog
首页
后端
分布式
前端
更多
分类
标签
归档
友情链接
关于
GitHub (opens new window)

Saul.J.Wu

立身之本,不在高低。
首页
后端
分布式
前端
更多
分类
标签
归档
友情链接
关于
GitHub (opens new window)
  • Java入门基础

    • 计算机常识

    • Java语言概述

    • 基本语法

    • 数组

    • 面向对象

      • 面向对象
      • 类和对象
      • 类的成员-属性
      • 类的成员-方法
      • 类的成员-构造器
      • 面向对象特性-封装
      • this关键字
      • package关键字
      • import关键字
      • 面向对象特征-继承
      • 方法的重写
      • 访问权限修饰符
      • super关键字
      • 子类对象实例化的全过程
      • 面向对象特征-多态
      • 强制类型转换
      • Object类
      • 包装类
      • static关键字
      • 单例设计模式
      • main方法
      • 类的成员-代码块
      • final关键字
      • 抽象类与抽象方法
      • 接口
      • 类的成员-内部类
        • 概述
        • 成员内部类
        • 局部内部类
        • 区别
        • 为什么需要内部类
        • 匿名内部类
    • 异常处理

  • Java核心基础

  • 设计模式

  • Web开发

  • SpringBoot

  • 微服务

  • Elasticsearch

  • 运维

  • 后端
  • Java入门基础
  • 面向对象
SaulJWu
2020-12-16

类的成员-内部类

类的成员之五:内部类

# 概述

  • 当一个事物的内部,还有一个部分需要一个完整的结构进行描述,而这个内部的完整的结构又只为外部事物提供服务,那么整个内部的完整结构最好使用内部类。
  • 在Java中,允许一个类的定义位于另一个类的内部,前者称为内部类,后者称为外部类。
  • Inner class一般用在定义它的类或语句块之内,在外部引用它时必须给出完整的名称。
    • Inner class的名字不能与包含它的外部类类名相同;
  • 分类
    • 成员内部类(static成员内部类和非static成员内部类)
    • 局部内部类(不谈修饰符)、匿名内部类

# 成员内部类

成员内部类作为类的角色

  • 可以在内部定义属性、方法、构造器等结构
  • 可以声明为abstract类,因此可以被其它的内部类继承
  • 可以声明为final的,言外之意没有final修饰的,可以被继承
  • 可以调用外部类的结构
  • 可以被4种权限修饰
  • 编译以后生成OuterClass$InnerClass.class字节码文件(也适用于局部内部类)
class Person{
    
    public void eat(){
        ……
    }
    
    //抽象
    abstract static Bird{
        
    }
    
    
    //静态成员内部类
    static class Dog{
        //定义属性
        String name;
        int age;
        //定义方法
        public void show(){
             ……
        }
    }
    
    //非静态成员内部类
    class Cat{
        //定义属性
        double weight;
        //定义构造器
        public Cat(){
            ……
        }
        
        
        //定义方法
        public void miao(){
            //调用外部类的方法
            eat();//实际上是Person.this.eat();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

成员内部类作为类的成员的角色

  • 和外部类不同,Inner class还可以声明为private或protected;
  • 可以调用外部类的结构
  • Innerclass可以声明为static的,但此时就不能再使用外层类的非static的成员变量;

# 局部内部类

class Person{
    public void method(){
        //方法内的局部内部类
        class AA{
            
        }
    }
    
    {
        //代码块内的局部内部类
        class BB{
            
        }
    }
    
    public Person(){
        //构造器内的局部内部类
        class CC{
            
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 区别

如何实例化成员内部类的对象

创建静态的成员内部类:

Person.dog dog = new Person.dog();
dog.show();
1
2

创建非静态的成员内部类

Person p = new Person();
Person.Cat cat = p.new Cat();
cat.miao();
1
2
3

如何在成员内部类区分外部类的结构

在内部类调用:

  • 形参直接使用
  • 内部类的属性或方法:this.属性/方法
  • 内部类调用外部类的属性或方法:外部类名.this.属性/方法

# 为什么需要内部类 (opens new window)

在某些业务场景下,需要重写某个类的方法,然后在这个方法体内使用,就可以使用局部内部类。

至此,我们已经看到了许多描述内部类的语法和语义,但是这并不能同答“为什么需要内部类”这个问题。那么,Java 设计者们为什么会如此费心地增加这项基本的语言特性呢?

一般说来,内部类继承自某个类或实现某个接口,内部类的代码操作创建它的外部类的对象。所以可以认为内部类提供了某种进入其外部类的窗口。

内部类必须要回答的一个问题是:如果只是需要一个对接口的引用,为什么不通过外部类实现那个接口呢?答案是:“如果这能满足需求,那么就应该这样做。”那么内部类实现一个接口与外部类实现这个接口有什么区别呢?答案是:后者不是总能享用到接口带来的方便,有时需要用到接口的实现。所以,使用内部类最吸引人的原因是:

每个内部类都能独立地继承自一个(接口的)实现,所以无论外部类是否已经继承了某个(接口的)实现,对于内部类都没有影响。

如果没有内部类提供的、可以继承多个具体的或抽象的类的能力,一些设计与编程问题就很难解决。从这个角度看,内部类使得多重继承的解决方案变得完整。接口解决了部分问题,而内部类有效地实现了“多重继承”。也就是说,内部类允许继承多个非接口类型(译注:类或抽象类)。

# 匿名内部类

下面的例子看起来有点奇怪:

// innerclasses/Parcel7.java
// Returning an instance of an anonymous inner class
public class Parcel7 {
    public Contents contents() {
        return new Contents() { // Insert class definition
            private int i = 11;

            @Override
            public int value() { 
                return i; 
            }
        }; // Semicolon required
    }

    public static void main(String[] args) {
        Parcel7 p = new Parcel7();
        Contents c = p.contents();
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

contents() 方法将返回值的生成与表示这个返回值的类的定义结合在一起!另外,这个类是匿名的,它没有名字。更糟的是,看起来似乎是你正要创建一个 Contents 对象。但是然后(在到达语句结束的分号之前)你却说:“等一等,我想在这里插入一个类的定义。”

这种奇怪的语法指的是:“创建一个继承自 Contents 的匿名类的对象。”通过 new 表达式返回的引用被自动向上转型为对 Contents 的引用。上述匿名内部类的语法是下述形式的简化形式:

// innerclasses/Parcel7b.java
// Expanded version of Parcel7.java
public class Parcel7b {
    class MyContents implements Contents {
        private int i = 11;
        @Override
        public int value() { return i; }
    }

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

    public static void main(String[] args) {
        Parcel7b p = new Parcel7b();
        Contents c = p.contents();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

在这个匿名内部类中,使用了默认的构造器来生成 Contents。下面的代码展示的是,如果你的基类需要一个有参数的构造器,应该怎么办:

// innerclasses/Parcel8.java
// Calling the base-class constructor
public class Parcel8 {
    public Wrapping wrapping(int x) {
        // Base constructor call:
        return new Wrapping(x) { // [1]
            @Override
            public int value() {
                return super.value() * 47;
            }
        }; // [2]
    }
    public static void main(String[] args) {
        Parcel8 p = new Parcel8();
        Wrapping w = p.wrapping(10);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  • [1] 将合适的参数传递给基类的构造器。
  • [2] 在匿名内部类末尾的分号,并不是用来标记此内部类结束的。实际上,它标记的是表达式的结束,只不过这个表达式正巧包含了匿名内部类罢了。因此,这与别的地方使用的分号是一致的。

尽管 Wrapping 只是一个具有具体实现的普通类,但它还是被导出类当作公共“接口”来使用。

// innerclasses/Wrapping.java
public class Wrapping {
    private int i;
    public Wrapping(int x) { i = x; }
    public int value() { return i; }
}
1
2
3
4
5
6

为了多样性,Wrapping 拥有一个要求传递一个参数的构造器。

在匿名类中定义字段时,还能够对其执行初始化操作:

// innerclasses/Parcel9.java
public class Parcel9 {
    // Argument must be final or "effectively final"
    // to use within the anonymous inner class:
    public Destination destination(final String dest) {
        return new Destination() {
            private String label = dest;
            @Override
            public String readLabel() { return label; }
        };
    }
    public static void main(String[] args) {
        Parcel9 p = new Parcel9();
        Destination d = p.destination("Tasmania");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

如果在定义一个匿名内部类时,它要使用一个外部环境(在本匿名内部类之外定义)对象,那么编译器会要求其(该对象)参数引用是 final 或者是 “effectively final”(也就是说,该参数在初始化后不能被重新赋值,所以可以当作 final)的,就像你在 destination() 的参数中看到的那样。这里省略掉 final 也没问题,但通常加上 final 作为提醒比较好。

如果只是简单地给一个字段赋值,那么此例中的方法是很好的。但是,如果想做一些类似构造器的行为,该怎么办呢?在匿名类中不可能有命名构造器(因为它根本没名字!),但通过实例初始化,就能够达到为匿名内部类创建一个构造器的效果,就像这样:

// innerclasses/AnonymousConstructor.java
// Creating a constructor for an anonymous inner class
abstract class Base {
    Base(int i) {
        System.out.println("Base constructor, i = " + i);
    }
    public abstract void f();
}
public class AnonymousConstructor {
    public static Base getBase(int i) {
        return new Base(i) {
            { 
                System.out.println("Inside instance initializer"); 
            }
            
            @Override
            public void f() {
                System.out.println("In anonymous f()");
            }
        };
    }
    public static void main(String[] args) {
        Base base = getBase(47);
        base.f();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

输出为:

Base constructor, i = 47
Inside instance initializer
In anonymous f()
1
2
3

在此例中,不要求变量 i 一定是 final 的。因为 i 被传递给匿名类的基类的构造器,它并不会在匿名类内部被直接使用。

下例是带实例初始化的"parcel"形式。注意 destination() 的参数必须是 final 的,因为它们是在匿名类内部使用的(译者注:即使不加 final, Java 8 的编译器也会为我们自动加上 final,以保证数据的一致性)。

// innerclasses/Parcel10.java
// Using "instance initialization" to perform
// construction on an anonymous inner class
public class Parcel10 {
    public Destination
    destination(final String dest, final float price) {
        return new Destination() {
            private int cost;
            // Instance initialization for each object:
            {
                cost = Math.round(price);
                if(cost > 100)
                    System.out.println("Over budget!");
            }
            private String label = dest;
            @Override
            public String readLabel() { return label; }
        };
    }
    public static void main(String[] args) {
        Parcel10 p = new Parcel10();
        Destination d = p.destination("Tasmania", 101.395F);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

输出为:

Over budget!
1

在实例初始化操作的内部,可以看到有一段代码,它们不能作为字段初始化动作的一部分来执行(就是 if 语句)。所以对于匿名类而言,实例初始化的实际效果就是构造器。当然它受到了限制-你不能重载实例初始化方法,所以你仅有一个这样的构造器。

匿名内部类与正规的继承相比有些受限,因为匿名内部类要么继承类,要么实现接口,但是不能两者兼备。而且如果是实现接口,也只能实现一个接口。

帮我改善此页面 (opens new window)
#内部类inner
上次更新: 2020/12/18, 12:50:58
接口
异常处理

← 接口 异常处理→

最近更新
01
zabbix学习笔记二
02-28
02
zabbix学习笔记一
02-10
03
Linux访问不了github
12-08
更多文章>
Theme by Vdoing | Copyright © 2020-2022 Saul.J.Wu | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式