Java基础 - 内部类

目录

引言

本文主要还是记录下内部类的基础语法,具体的作用等接触更多的代码之后回来小结

定义与用法

内部类,顾名思义就是在一个类的内部再定义一个类

public class Outer {
    private String id;

    public Outer(String id) {
        this.id = id;
    }

    /**
     * 内部类可以是任意的访问权限
     */
    public class Inner {
        // 可以任意的访问外部的属性
        private String innerId = "inner" + id;
    }
}

访问权限

  1. 内部类本身可以设置任意的访问权限(private,default,protected,public
  2. 内部类可以访问外部类任意权限的属性或方法(普通内部类)
    正是由于内部类可以定义为private,所以内部类的作用之一就是可以对外屏蔽类的存在
    这一点在内部类实现接口时作用会比较明显,例如说Iterator就是一个比较典型的例子
     List<String> list = new ArrayList<>();
     Iterator<String> iterator = list.iterator();
    
    对于客户端来说,你拿到的是一个Iterator,而非ArrayList的内部类对象Itr,你能访问的也只有Iterator接口所提供的方法
    也就是说,ArrayList提供了迭代器的功能,但是完全对客户端屏蔽其实现
  3. 非静态内部类不能定义静态成员,除非它是final(并且是编译期常量)的
    为什么非静态内部类中不能定义静态成员,为什么final又是可以的,参考下面这篇文章:
    为什么非静态内部类中不能有static属性的变量,却可以有static final属性的变量?
  4. 静态内部类不能访问外部类的非静态成员(就好比说静态方法不能访问非静态成员一个意思)

内外通信

这一小结主要就是说明访问外部属性以及创建内部类对象的方法

.this

普通的内部类(非静态内部类)之所以能够访问外部类的所有属性,肯定是因为有一个应用指向了外部类

如果你需要生成对外部类对象的引用,可以使用外部类的名字后面紧跟 .this --- 《OnJava8》

  1. 如果直接使用this,则this指向的是内部类的对象,外部类 + .this才是指向外部类的对象
  2. 访问外部属性时并非都要加上.this
    例如下面的代码示例中我们使用Outer.this.getId(),其实直接调用getId()即可
    但有时这时必要的,比如说内部类和外部类的方法、属性重名了,我们在访问外部类时就必须使用 【外部类 + .this】,否则就无限循环了
public class Outer {
    private String id;

    public Outer(String id) {
        this.id = id;
    }

    private String getId() {
        return this.id;
    }

    private void setId(String id) {
        this.id = id;
    }

    /**
     * 内部类可以是任意的访问权限
     */
    public class Inner {
        // 可以任意的访问外部的属性
        private String innerId = "inner" + id;

        public String getOutId() {
            return getId();
        }

        public void setId(String id) {
            Outer.this.setId(id);
        }
    }
}

.new

创建内部类对象时必须要使用外部类的对象进行创建

public class OuterTest {
    public static void main(String[] args) {
        Outer outer = new Outer("张三");
        Outer.Inner inner = outer.new Inner();
        System.out.println("inner class getId: " + inner.getId());
    }
}

局部内部类

有时一个内部类的对象只在某个方法中使用,我们可以在方法中定义内部类
局部内部类的另一个优点就是可以访问方法中的局部变量(必须事实上是final的,即在匿名类外部赋值后不能再改变了)

匿名内部类

在局部内部类的基础上再进一步,有时我们不需要创建多个内部类对象,只需要一个,那么使用匿名内部类会更加方便
例如我们在Java多线程中使用Runnable接口时就是如此。

demo

假如说有一个计算接口

public interface Calculate {
    int cal(Integer num1, Integer num2);
}

我们想实现一个加法器

  • 常规内部类写法
public class Outer3 {
    class Adder implements Calculate{
        @Override
        public int cal(Integer num1, Integer num2) {
            return num1 + num2;
        }
    }

    public Calculate getAdder() {
        return new Adder();
    }
}

测试一下:

 Outer3 outer3 = new Outer3();
 Calculate adder = outer3.getAdder();
 int r1 = adder.cal(1,2); // 3
  • 匿名内部类写法
public class Outer3 {
    public Calculate getAdder() {
        return new Calculate() {
            @Override
            public int cal(Integer num1, Integer num2) {
                return num1 + num2;
            }
        };
    }
}

同样的测试:

Outer3 outer3 = new Outer3();
Calculate adder = outer3.getAdder();
int r1 = adder.cal(1,2); // 3
  • 匿名内部类 + Java8
    由于我们的Calculate只有一个接口,所以我们还可以用Java8的lambda表达式再简化一下
public class Outer3 {
    public Calculate getAdder() {
        return (num1, num2) -> num1 + num2;
    }
}

测试方法还是不变

Outer3 outer3 = new Outer3();
Calculate adder = outer3.getAdder();
int r1 = adder.cal(1,2);  // 3
  • 直接写在方法里面
    当这个内部类的对象并不需要复用的地方,我们也可以直接在使用到它的地方编写匿名内部类对象
Calculate adder = (num1, num2) -> num1 + num2;
int calRes = adder.cal(1,2);   // 3

初始化

匿名内部类因为没有名字,所以你没有办法通过构造函数进行初始化,但我们还是有一些其他的方法来达到相同的效果

  • 继承一个基类
    还是用上面那个加法器的例子
public abstract class AbstractCalculate implements Calculate {
    public int baseNum;
    public AbstractCalculate(int i) {
        baseNum = i;
    }
}

测试一下

  // 此处的 1 可修改为从外部传参
  Calculate adder = new AbstractCalculate(1) {
      @Override
      public int cal(Integer num1, Integer num2) {
          return num1 + num2 + baseNum;
      }
  };
  int res = adder.cal(1,2);  // 4

即通过父类来进行初始化并使用父类的成员

  • 代码块
    继承的方式要求必须有一个父类,有一定的限制,另外一种方法就是通过代码块的方式
public class Outer3 {

    public Calculate getAdder() {
        return (num1, num2) -> num1 + num2;
    }

    // 这里我们在初始化的时候做一些复杂的事情,比如初始化一个对象
    public Calculate getAdderWithInit(int base) {
        Calculate adder = new Calculate() {
            Calculate _adder;
            {
                _adder = getAdder();
            }
            @Override
            public int cal(Integer num1, Integer num2) {
                return _adder.cal(num1,num2) + base;
            }
        };
        return adder;
    }

}

测试一下:

Calculate adder = new Outer3().getAdderWithInit(2);
int res = adder.cal(1,2); // 5

静态内部类

有时我们只是为了把一个类隐藏在另一个类中,并不需要内部类应用外部类的对象,此时我们就可以将内部类声明为static的

普通的内部类对象隐式地保存了一个引用,指向创建它的外部类对象。然而,当内部类是 static 的时,就不是这样了。

嵌套类(静态内部类)意味着:

  1. 创建嵌套类的对象时,不需要其外部类的对象。
  2. 不能从嵌套类的对象中访问非静态的外部类对象。

嵌套类与普通的内部类还有一个区别:
普通内部类的字段与方法,只能放在类的外部层次上,所以普通的内部类不能有 static 数据和 static 字段,也不能包含嵌套类。
但是嵌套类可以包含所有这些东西:

接口内部类

嵌套类可以作为接口的一部分。你放到接口中的任何类都自动地是 public 和 static 的。因为类是 static 的,只是将嵌套类置于接口的命名空间内,这并不违反接口的规则。你甚至可以在内部类中实现其外部接口
如果你想要创建某些公共代码,使得它们可以被某个接口的所有不同实现所共用,那么使用接口内部的嵌套类会显得很方便
---- 《OnJava8》

内部类作用小结

命名

使用内部类命名可以避免命名重复的问题
例如Element这样一个类,你在很多类中可能都会使用到,为了避免重复你可能会使用AElement,BElement这种方式,但是使用内部类则可以使代码结构更加清晰一些

代码结构

  1. 当两个类的关系特别紧密时,通过内部类可以使得代码结构更加清晰
  2. 同时外部类可以少传递很多参数,因为内部类可以直接访问外部类的成员

如果不使用外部类,我们就需要使用组合的方式来实现对象的复用,相比使用内部类的缺点:

  1. 内部类的对象其实只有外部类会使用,但如果不使用内部类的方式,你就无法用private来修饰类,最多只能用default,即将这个类暴露给其他类了
  2. 外部类使用内部类的对象方法时需要传递很多参数过去

访问控制

即在访问权限中提到过的,可以隐藏内部类,当我的内部类实现了某个接口时,则相当于外部类隐藏了接口的实现,比较典型的就是Iterator

"多重继承"

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

  1. 内部类实现接口:如果内部类只是实现接口,那么由外部类自己实现,功能上其实并没有太大的区别
  2. 内部类继承抽象类:这个就只有内部类才能完成了,因为Java是单继承的体系

参考资料

《OnJava8》
《Java核心技术:卷1》

上一篇:迭代法-牛顿迭代法


下一篇:1689. Partitioning Into Minimum Number Of Deci-Binary Numbers