• 你了解Java的内部类吗


    系列文章:该文章为第一篇

    前言

      内部类顾名思义指的就是一个类声明在另一个类的内部,我们在平时读源码时或多或少见过内部类,但自己写的代码中可能基本不使用或很少使用他们,即使用了可能也不太清楚为什么要使用,只知道可以使用。

      文章开始之前先罗列一下针对内部类可能提出的问题:

    • 为什么要使用内部类,什么时候使用内部类
    • 为什么有时候必须使用static标注内部类,有时候却不用,他们之间有什么区别
    • 内部类的属性以及方法访问规则是什么
    • IDE报错信息或警告中有涉及到内部类相关的概念,但是比较混淆,甚至不清楚什么意思。如:
      在这里插入图片描述

      带着这些问题,我们开始重新学习一下内部类的相关知识。

      注:本文的内容以及例子是JDK8进行编写,其他版本可能会有所区别。

    相关概念

    • Nested class
    • Inner Class
    • Static Nested Class
    • Local Inner Class
    • Anonymous Inner Class
    • Enclosing Class
    • Outer Class

    正文

      我们平时所说的内部类,实际上是一个广义的概念,官方对它的定义如下:

    The Java programming language allows you to define a class within another class. Such a class is called a nested class.
    Java编程语言允许在一个类的内部定义另一个类,这样的一个类被称为嵌套类(Nested Class)

      以上内容引用于Java官方文档-Nested Classes一节中,由此可知我们平时所说的内部类实际上官方的话术称之为嵌套类

      嵌套类又分为两种,静态和非静态。其中不被static关键字修饰的嵌套类被称为内部类(Inner Class),被static关键字修饰的嵌套类被称为静态嵌套类(Static Nested class)。一个内部定义了嵌套类的类被称作封闭类/外部类(Enclosing Class),有时也被称作Outer Class

      嵌套类是外部类的一个成员,同其他成员变量一样,嵌套类可以声明为public, protected, private或包内私有(无修饰符),这一点与普通类不一样,普通类只能声明为public或包内私有。

    Inner Class内部类

    概念

       不被static关键字修饰的嵌套类称之为Inner Class,即内部类。

    public class EnclosingClass {
    
      public class InnerClass {
      
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    创建实例

      我们知道,一个类声明的成员变量和成员方法都是隶属于某一个类的实例的,同成员变量和成员方法,内部类也是隶属于它的外部类实例。这意味着,必须先创建外部类的实例,然后才能创建该外部类实例下的内部类实例,例如:

    EnclosingClass enclosingInstance = new EnclosingCLass();
    EnclosingClass.InnerClass innerInstance = enclosingInstance.new InnerClass();
    
    // 也可以直接写作
    EnclosingClass.InnerClass innerInstance2 = new EnclosingClass().new InnerClass();
    
    • 1
    • 2
    • 3
    • 4
    • 5

      上例中可以看出:

    • 内部类的声明为外部类.内部类,这样可以和同名的顶级类或其他嵌套类区分开来。
    • 实例化的方式为外部类实例.new 内部类()

      在外部类实例中实例化内部类时,可以直接使用new 内部类()的形式

    public class EnclosingClass {
    
      private int outerField;
    
      private void outerMethod() {
        InnerClass innerClass = new InnerClass();
      }
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    访问规则

      1. 由于内部类实例所属于外部类实例,所以外部类实例可以直接访问内部类实例的任意访问修饰符修饰的变量以及方法,因为他们都是外部类实例的一部分;同样的,内部类实例也可以直接访问外部类实例的任意访问修饰符修饰的变量以及方法,因为他们都是同级别(可以对比理解,一个类实例的方法可以访问实例中的另一个方法或变量,即使可能被private修饰),如:

    public class EnclosingClass {
    
      private int outerField = 123;
      private final InnerClass innerInstance;
    
      public EnclosingClass() {
        innerInstance = new InnerClass();
      }
    
      public void outerMethod() {
        // 外部类方法中访问内部类的私有变量
        innerInstance.innerField = "abc";
        System.out.println(innerInstance.innerField);
        // 外部类方法中访问内部类的私有方法
        innerInstance.innerPrivateMethod();
      }
    
      private void outerPrivateMethod() {
        System.out.println("outer method");
      }
    
      public class InnerClass {
    
        private String innerField;
    
        public void innerMethod() {
          // 内部类方法中访问所属外部类实例的私有变量
          System.out.println(outerField);
          // 内部类方法中访问所属外部类实例的私有方法
          outerPrivateMethod();
        }
    
        private void innerPrivateMethod() {
          System.out.println("inner method");
        }
      }
    }
    
    • 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

      2. 由于内部类隶属于属于外部类实例,所以无法在内部类中声明任何静态变量或静态方法,因为此时还未创建外部类实例,但是可以创建静态常量。

    public class EnclosingClass {
    
      public class InnerClass {
        
        // 编译成功
    	public static final String DELIMITER = "|";
    	// 编译错误
        public static String str = "str";
        
       	// 编译错误
        public static void test() {
    
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

      注:JDK16以及以上版本支持此种写法,但不在本文的讨论范围内。
      3. 内部可以调用其他类的静态变量或方法:

    public class EnclosingClass {
    
      private static final String DELIMITER = "|";
        
      public static int staticMethod() {
        return 0;
      }
      
      public class InnerClass {
    
        private String str = "inner";
    
        public void test() {
          System.out.println(DELIMITER);
          System.out.println(staticMethod());
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    影子变量

      调用一个方法的完整写法应为实例.方法();,在一个方法中如果调用本类中的另一个方法,我们可以直接调用,如:

    public class Demo {
      
      public void method1() {
        method2();
      }
      
      public void method2() {
        // Do something
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

      有些情况下想显示的指明是当前方法所属实例的方法时,会使用this关键字:

    public class Demo {
    
      private boolean greaterThanZero;
      public Demo(int number) {
        // 显示指定当前实例的成员变量greaterThanZero
        this.greaterThanZero = (number > 0);
      }
      
      public void method1() {
        // 显示调用当前实例的method2方法
        this.method2();
      }
      
      public void method2() {
        // Do something
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

      同理,如果this关键字出现在内部类中,就表示this关键字会指向当前内部类实例,如

    public class EnclosingClass {
    
      private String str = "outer";
    
      public class InnerClass {
    
        private String str = "inner";
    
        public void test() {
          System.out.println(this.str);
        }
      }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
      public static void main(String[] args) {
        new EnclosingClass().new InnerClass().test();
      }
    
    • 1
    • 2
    • 3

      此时,在内部类的test()方法中,访问的是内部类中的str变量,而不是外部类的str变量,因为this指代了当前方法所属实例,即内部类实例。需要明确的是,外部类的成员变量或方法对内部类来说有着访问权,但是未拥有所属权,换句话说就是使用this并不能够访问到外部类的变量或方法。上例中,外部类和内部类都有各自的成员变量str,并且他们的名字相同,如果想访问外部类的str,有两种方法,第一种就是内部类持有一个外部类实例变量,第二种方法就是使用外部类.this来显示指明使用外部类实例的变量或方法, 称作影子变量(shadowing), 如:

    public class EnclosingClass {
    
      private String str = "outer";
    
      public class InnerClass {
    
        private String str = "inner";
    
        public void test() {
          // 使用shadowing影子变量来获取外部类的str
          System.out.println(EnclosingClass.this.str);
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    其他

      有两种特殊的内部类:本地内部类以及匿名内部类,会在后续文章中介绍。

    Static Nested Class

    概念

       被static关键字修饰的嵌套类称之为Static Nested Class,即静态嵌套类,也就是通常我们所说的静态内部类。

    public class EnclosingClass {
    
      public static class StaticNestedClass {
        
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    创建实例

       正如类的静态变量以及静态方法无将类实例化就可以直接调用,静态嵌套类也是一样,就如同顶级类一样:

      EnclosingClass.StaticNestedClass staticNestedInstance = new EnclosingClass.StaticNestedClass();
    
    • 1

       如上例所示,无需先创建外部类实例便可以直接创建静态嵌套类的实例,声明方式同内部类: 外部类.静态嵌套类
      在外部类实例中实例化内部类时,可以直接使用new 静态嵌套类()的形式

    public class EnclosingClass {
      
      public void test() {
        StaticNestedClass staticNestedInstance = new StaticNestedClass();
      }
    
      public static class StaticNestedClass {
    
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    访问规则

      1. 与内部类相反,由于静态嵌套类不需要先实例化外部类,所以静态嵌套类是无法直接访问外部类的实例成员的,需要像顶级类一样使用实例.变量以及实例.方法()才可以,但是此时可以访问私有的变量以及方法:

    public class EnclosingClass {
    
      private String instanceField = "abc";
    
      private void instanceMethod() {
        // Do something
      }
    
      public static class StaticNestedClass {
    
    
        public void test() {
          // 编译错误,无法调用实例成员变量
          System.out.println(instanceField);
          // 编译错误,无法调用实例方法
          instanceMethod();
        }
        
        // 正确
        public void test2(EnclosingClass instance) {
          System.out.println(instance.instanceField);
          instance.instanceMethod();
        }
      }
    }
    
    • 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

      2. 可以访问外部类的静态变量或方法(包含private)

    public class EnclosingClass {
    
      private static String staticField = "abc";
    
      private static void staticMethod() {
        // Do something
      }
    
      public static class StaticNestedClass {
    
        public void test() {
          System.out.println(staticField);
          staticMethod();
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    为什么/什么时候要使用嵌套类

    • 当某个类仅在一处使用时,可以选择使用嵌套类,这种情况也可以将该嵌套类视为一个helper class。使用private关键字修饰该嵌套类,可以减少该嵌套类不必要的暴露,并且可以减少.java文件的数量(但并未减少.class的数量)。
      如一个类的方法想返回多个值,并且只在本类中使用,那么可以有如下写法:
    public class Demo {
      
      public UserInfo getUserInfo() {
        UserInfo userInfo = new UserInfo();
        // 赋值
        return userInfo;
      }
      
      // 使用私有的静态嵌套类
      private static class UserInfo {
        private String name;
        private int age;
        private String address;
      } 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 更好的封装:当类B想访问A的私有成员时,可以考虑将B设计为A的嵌套类,这样B就可以访问到A的私有成员了,避免了对外暴露他们。
    public class Demo {
    
      private String id;
    
      private class HttpInvoker {
    
        public void sendHttpRequest() throws Exception {
          URL url = new URL("http://xxxx/?id=" + id);
          // Do something...
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 可以提高阅读性:可以给阅读者直接的感受,即该嵌套类与外部类是有紧密联系的,如Feign.BuilderMap.Entry等。

    使用内部类还是静态嵌套类

      我们可以根据各自的特点来选择合适的嵌套类

    • 如果想在嵌套类中访问外部类实例的成员变量/方法等,那么就使用内部类。
    • 否则使用静态嵌套类。

      IDEA也会提示你应该使用什么样的嵌套类,如使用了内部类,但是并未调用外部类的成员时,会有如下提示:
    在这里插入图片描述

  • 相关阅读:
    C++标准模板(STL)- 输入/输出操纵符-(std::get_money,std::put_money)
    Python第11章 时间序列
    R语言ggplot2可视化:使用patchwork包将两个ggplot2可视化结果横向构成新的结果可视化组合图(使用|符号)
    高压配电柜电力安全监控解决方案
    怎么视频抠图?一键AI智能抠图,这招你一定要学会
    C++面向对象 _ 成绩单系统
    我的两周年创作纪念日
    Eclipse切换中文环境
    【Mysql】给查询记录增加序列号方法
    vue3 vue.config.js分包配置
  • 原文地址:https://blog.csdn.net/csdn_mrsongyang/article/details/122376173