【Java】接口和内部类

接口

接口的概念

现实生活中接口存在于很多地方,例如:USB接口,插座

我们可以发现,接口是可以适用于符合其标准的设备的,什么意思呢?比如USB接口,标准就是USB,那么只要是符合USB标准的设备就可以插入进来。例如我们的笔记本电脑上的USB接口,就可以插入鼠标、键盘、U盘这样符合USB标准的设备。

而这个标准,可以被描述为是一种行为规范,也就是能够做某事,比如USB接口的的标准就是能够插入USB设备,如果是什么type-c,lighting之类的就插不进去。

在Java中,我们就可以通过定义接口来定义一种行为规范,说明一个类能够做某事

接口的语法

接口的定义和类的定义十分相似,语法如下

访问修饰符 interface 接口名 {
    
    
    成员变量;
    成员方法;
}

接口中的成员变量,是自带public static final的,而成员方法则是自带public abstract,并且接口中的方法除了两个特例以外,其他方法均不可以有具体的方法体

两个特例:1. Java8开始,接口中允许定义一个有具体实现的default方法 2. static修饰的方法可以有具体的实现

下面是一些接口的注意点:

  1. 接口并不是类而是一种独立的类型,类和类之间通过继承来实现多态,而类和接口是通过实现来实现多态
  2. 接口中不能有代码块
  3. 接口和类一样会产生独立的字节码文件

接口使用

接口的实现

我们可以通过实现关键字implements,来让类和接口产生联系(就类似于继承中的extend

扫描二维码关注公众号,回复: 17242450 查看本文章
interface ITest{
    
    
    //接口命名一般在原义的名字前加一个大写I,但实际上也无所谓
    void test();
}
public class Test implements ITest{
    
    
    //实现ITest接口
    @Override
    public void test() {
    
    
        System.out.println("hehe");
    }
}

上面我们说,接口是用于描述一个类能够做某事的,那么具体是怎么一个用法呢,那么下面通过例子说明

比如我们写一个狗狗类,并且用接口描述它能够吃东西,那么写出来就是

interface IEating{
    
    
    void eat();
}
public class Dog implements IEating{
    
    
    String name;
    public Dog(String name){
    
    
        this.name = name;
    }
    @Override
    public void eat() {
    
    
        System.out.println(this.name + "正在吃东西");
    }
}

也就是说,我们可以不通过继承关系描述类于类之间的从属关系,而是通过接口描述一个类能够做某事,从而形成多态

下面就是一个能够展现出接口的实现和类的继承有何不同的代码

注意:接口的实现一定要写在类的继承后面

//定义一个抽象类和接口
abstract class Animal {
    
    
    public abstract void eat();
}
interface IRunning {
    
    
    void run();
}

//下面实现一个猫咪类和狗狗类 继承Animal类并实现IRunning接口
class Cat extends Animal implements IRunning{
    
    
    String name;

    public Cat(String name) {
    
    
        this.name = name;
    }

    @Override
    public void eat() {
    
    
        System.out.println(this.name + "正在吃东西");
    }

    @Override
    public void run() {
    
    
        System.out.println(this.name + "正在跑步");
    }
}

class Dog extends Animal implements IRunning{
    
    
    String name;

    public Dog(String name) {
    
    
        this.name = name;
    }
    @Override
    public void eat() {
    
    
        System.out.println(this.name +"正在吃东西");
    }

    @Override
    public void run() {
    
    
        System.out.println(this.name +"正在跑步");
    }
}
//定义一个机器人类,并实现IRunning接口
class Robot implements IRunning{
    
    

    @Override
    public void run() {
    
    
        System.out.println("机器人正在跑步");
    }
}

通过上面代码,我们可以看出,机器人和猫咪狗狗都可以跑步,因此我们通过接口来描述它们能够跑步

但是机器人不属于动物,所以我们不将它加入以Animal类为父类的继承体系中

因此,类和类之前的继承是用来描述从属关系的,比如上面的代码中,在动物类中定义一个eat()方法,意思就是你是动物,你就能吃东西,因此让猫狗类都继承了Animal类并且重写了eat()方法

而接口是用来描述一个类是否能够做某事的,比如上面的代码中,是动物也不一定能跑步,不是动物也不一定不能跑步,因此将跑步这个特性提取出来作为接口,由于机器人,猫狗都可以跑步,所以都给他们实现了IRunning接口

多接口实现

我们在上面就多次强调,接口描述的是一个类能够做某事,但是在现实生活中,往往有一些类能够做很多事,因此Java中一个类可以实现多个接口

下面是一个多接口实现的例子

interface IRunning {
    
    
    void run();
}
interface IEating {
    
    
    void eat();
}
class Dog implements IEating,IRunning{
    
    
    String name;

    public Dog(String name) {
    
    
        this.name = name;
    }

    @Override
    public void eat() {
    
    
        System.out.println(this.name + "正在吃东西");
    }

    @Override
    public void run() {
    
    
        System.out.println(this.name + "正在跑步");
    }
}

接口的继承

接口之间是可以继承的,并且支持多继承,接口的继承实际上就相当于接口的合并或者接口的拓展

实现了继承接口的类要将所有被继承接口和继承接口一同实现

例如下面代码

interface IRunning {
    
    
    void run();
}
interface ISwimming {
    
    
    void swim();
}

interface IAmphibious extends IRunning, ISwimming {
    
    
}
class Frog implements IAmphibious {
    
    
    String name;

    public Frog(String name) {
    
    
        this.name = name;
    }

    @Override
    public void run() {
    
    
        System.out.println(name + "正在跑步");
    }

    @Override
    public void swim() {
    
    
        System.out.println(name + "正在游泳");
    }
}

上面定义了一个两栖接口,表示既可以游泳也可以跑步,然后在青蛙类中实现了这个接口

接口参数类型

接口作为方法的参数类型的时候,就可以接收实现了这个接口的类型

并且可以通过这个接口参数调用实现接口时重写的方法

interface Eating{
    
    
    void eat();
}
class Dog implements Eating{
    
    

    @Override
    public void eat() {
    
    
        System.out.println("小狗吃东西");
    }
}
class Cat implements Eating{
    
    

    @Override
    public void eat() {
    
    
        System.out.println("小猫吃东西");
    }
}

public class Test2 {
    
    
    //使用接口参数,接收实现了这个接口的类型的对象
    public static void testEat(Eating e){
    
    
        e.eat();
    }
    public static void main(String[] args) {
    
    
        Dog dog = new Dog();
        Cat cat = new Cat();
        testEat(dog);
        testEat(cat);
    }
}

但是不能调用其他的方法,只能调用接口里有的方法

这点和继承类似,以父类为参数类型的时候,只能通过这个父类对象调用子类中的重写方法,不能调用子类特有的方法

而以接口为参数类型的时候,只能通过这个接口参数调用类实现接口时重写的方法,不能调用类中特有的方法

接口和抽象类区别

抽象类 接口
成员变量 可以有普通的成员变量 所有成员变量均是由public static final修饰
成员方法 可以有抽象和普通的成员方法 所有成员方法均以public abstract修饰
成员权限 可以是任何权限 只能是public
多态实现方式 通过extends继承 通过implements实现
继承数量 抽象类只能继承一个类 接口可以继承多个接口

始终要牢记,抽象类只不过比一般类多了一个可以写抽象方法的功能,因此可以更方便的用来继承。但是由于抽象类的本质就是类,也是可以描述对象的。而接口就是为了描述类的属性:能够做某事,只能用于描述对象的属性。

接口使用实例

Comparable接口

我们在之前讲Object类的时候讲过,重写equals()方法就可以判断对象是否相等,但是有时候仅仅只判断是否相等并无法满足我们的需求

例如下面我们需要将这个学生类的数组排序,那么要怎么做呢?

class Student{
    
    
    String name;
    int age;
    public Student(String name, int age) {
    
    
        this.name = name;
        this.age = age;
    }
}

public class Test {
    
    
    public static void main(String[] args) {
    
    
        Student[] students = new Student[3];
        students[0] = new Student("zhangsan",25);
        students[1] = new Student("lisi",15);
        students[2] = new Student("wangwu",35);
    }
}

我们先通过之前学过的Arrays.sort()来试试

import java.util.Arrays;

class Student{
    
    
    String name;
    int age;

    public Student(String name, int age) {
    
    
        this.name = name;
        this.age = age;
    }

}

public class Test {
    
    
    public static void main(String[] args) {
    
    
        Student[] students = new Student[3];
        students[0] = new Student("zhangsan",25);
        students[1] = new Student("lisi",15);
        students[2] = new Student("wangwu",35);
        Arrays.sort(students);
    }
}

会发现很明显是不行的,那么是为什么呢?

这个类是我们自定义的,编译器并不知道怎么去比较它new出的对象,也就肯定不会对它的数组进行排序了。因此我们如果想要排序它,那就要实现一个接口,告诉编译器,我们这个类是可以比较的,并且在里面实现这个接口的比较方法,从而让编译器知道怎么进行比较

而这个接口就是comparable接口,当我们去看它的源码的时候,会发现它的名字后面有一个<T>

public interface Comparable<T> {
    
    
    public int compareTo(T o);
}

我们现在先简单了解一下这个机制,会用带有这种机制的类和接口就行

这个是Java的一种机制,叫做泛型。它允许在定义类和接口的时候,定义一个类型参数,这个参数的使用就类似于方法中的形参和实参,比如我们下面要比较Student类,那么就把这个Student类型传过去

我们先写一个以年龄大小为比较基准的比较方法,帮助我们简单理解一下这个机制如何使用

//尖括号中填的是下面方法中要比较的类型
class Student implements Comparable<Student>{
    
    
    String name;
    int age;

    public Student(String name, int age) {
    
    
        this.name = name;
        this.age = age;
    }

    @Override
    //这里方法的参数类型要对应类型参数的类型
    public int compareTo(Student s) {
    
    
        return this.age - s.age;
    }
}

实际上如果你不写这个泛型,那么方法中参数类型就可以写成Object类型,但是不推荐这样做

如果现在无法理解,就可以简单理解为当你某个类继承的类或实现的接口中有尖括号,那么尖括号中就填这个类,然后重写方法的时候将源码中类型为T的方法全部换成填的类型(或者直接用编译器帮忙重写)

例如上面的Student实现的Comparable接口中有尖括号,那么尖括号里面就填Student,然后重写方法的参数类型也写成Student


关于我们上面写的比较方法,我们来看一下为什么要这样写

public int compareTo(Student s) {
    
    
    return this.age - s.age;
}

我们看一下Comparable接口中对于compareTo这个方法返回值的描述

a negative integer, zero, or a positive integer as this object is less than, equal to, or greater than the specified object.

也就是说返回一个负值、零或者正数,来表示this对象是小于、等于还是大于传入的对象

用上面的例子来说,如果返回值为负,则this.age < s.age其他同理


那么实现了接口,我们现在就可以对这个自定义类型的数组进行排序了

import java.util.Arrays;

class Student implements Comparable<Student>{
    
    
    String name;
    int age;

    public Student(String name, int age) {
    
    
        this.name = name;
        this.age = age;
    }

    @Override
    public int compareTo(Student s) {
    
    
        return this.age - s.age;
    }

    @Override
    public String toString() {
    
    
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

public class Test {
    
    
    public static void main(String[] args) {
    
    
        Student[] students = new Student[3];
        students[0] = new Student("zhangsan",25);
        students[1] = new Student("lisi",15);
        students[2] = new Student("wangwu",35);
        Arrays.sort(students);
        System.out.println(Arrays.toString(students));
    }
}

那么我们也可以通过冒泡排序算法,实现一个能够接收实现了comparable接口的类的排序方法

public class Test {
    
    
    public static void bubbleSort(Comparable<Student>[] c){
    
    
        for(int i = 0; i < c.length; i++){
    
    
            for (int j = 0; j < c.length - i - 1; j++) {
    
    
                //这里注意,我们实现的compareTo()方法接收的是Student类型
                //而我们传入的是接口类型,算作是向下转型,所以要进行强制类型转换
                if(c[j].compareTo((Student) c[j+1]) > 0){
    
    
                    Comparable<Student> tmp = c[j];
                    c[j] = c[j + 1];
                    c[j + 1] = tmp;
                }
            }
        }
    }
    public static void main(String[] args) {
    
    
        Student[] students = new Student[3];
        students[0] = new Student("zhangsan",25);
        students[1] = new Student("lisi",15);
        students[2] = new Student("wangwu",35);
        bubbleSort(students);
        System.out.println(Arrays.toString(students));
    }
}

Comparator接口

上面的代码我们实际上可以发现一些缺陷,比如当我们想要换一个比较方法的时候,那么就只能把原来的删了替换

那么有没有一种办法,可以实现多种比较方法共存呢,当我想要用哪个就调用哪个

Comparator接口就可以帮助我们解决这个问题,它用于说明一个类可以用来比较两个对象,但是它是直接将传入的两个对象进行比较,不像Comparable里的方法是调用者和传入的对象进行比较

可以将实现了Comparator接口的类直接理解为一个用于比较的工具,也就是Comparator比较器

那么我们就可以不在实现Comparable接口的类里面直接实现比较方法,而是通过创建比较器写好比较方法,在比较的时候调用需要的比较器即可

class AgeComparator implements Comparator<Student> {
    
    
    @Override
    public int compare(Student s1, Student s2) {
    
    
        return s1.age - s2.age;
    }
}

class NameComparator implements Comparator<Student> {
    
    
    @Override
    public int compare(Student s1, Student s2) {
    
    
        //String类自带comparaTo()方法直接用
        return s1.name.compareTo(s2.name);
    }
}

然后我们就可以直接在实现Comparable接口里的重写方法中调用需要的比较方法

public int compareTo(Student s) {
    
    
    NameComparator cmp = new NameComparator();
    return cmp.compare(this,s);
}

Clonable接口

Clonable接口是一个空接口,也被称作标记接口,它的内部没有任何方法,就如它的定义一样,是一个空的接口。它只用于说明这个类可以被克隆,但是克隆又是什么意思呢?

Object类中有一个方法clone(),它能够帮助我们将当前对象进行拷贝,也就是所谓的克隆,我们先看一下这个方法的源码

protected native Object clone() throws CloneNotSupportedException;

我们可以发现,它是一个protect方法,由于它和我们的工程不在一个包里面,因此我们只能通过super来调用它,然后接收它的返回值再进行操作

那么具体应该怎么操作这个方法呢?

我们通过一个例子一步步的进行讲解,假设我们要拷贝下面的这个对象p1

class Person{
    
    
    int age;
    public Person(int age) {
    
    
        this.age = age;
    }
}
public class Test {
    
    
    public static void main(String[] args) {
    
    
        Person p1 = new Person(100);
    }
}

首先我们要在需要被拷贝的对象的对应类里面重写clone()方法,让它返回一个super.clone()。在该例子中,就是在Person类里重写clone()方法,并让它返回super.clone()

class Person{
    
    
    int age;
    public Person(int age) {
    
    
        this.age = age;
    }
    public Object clone(){
    
    
        return super.clone();
    }
}

这个时候发现在报错,我们重新看一下clone()的源码会发现,在后面有一串throws CloneNotSupportedException

这个是什么意思呢?具体我们在异常章节讲解,我们现在先简单理解为,克隆方法被使用的时候,那个使用克隆的方法需要加上throws CloneNotSupportedException,如下

class Person{
    
    
    int age;
    public Person(int age) {
    
    
        this.age = age;
    }
    public Object clone() throws CloneNotSupportedException {
    
    
        return super.clone();
    }
}
public class Test {
    
    
    public static void main(String[] args) throws CloneNotSupportedException {
    
    
        Person p1 = new Person(100);
        Person p2 = p1.clone();
    }
}

由于我们克隆方法的返回值是Object类型,因此要存入Person类要进行向下转型,也就是强制类型转换

class Person{
    
    
    int age;
    public Person(int age) {
    
    
        this.age = age;
    }
    public Object clone() throws CloneNotSupportedException {
    
    
        return super.clone();
    }
}
public class Test {
    
    
    public static void main(String[] args) throws CloneNotSupportedException {
    
    
        Person p1 = new Person(100);
        Person p2 = (Person) p1.clone();
    }
}

这个时候运行发现还是在报错,报错的内容是不支持克隆。也就是说我们这个类并不支持克隆,那么此时我们给Person类实现Clonable接口即可

class Person implements Cloneable{
    
    
    int age;
    public Person(int age) {
    
    
        this.age = age;
    }
    public Object clone() throws CloneNotSupportedException {
    
    
        return super.clone();
    }
}
public class Test {
    
    
    public static void main(String[] args) throws CloneNotSupportedException {
    
    
        Person p1 = new Person(100);
        Person p2 = (Person) p1.clone();
    }
}

那么此时就算是大功告成,将p1的内容克隆给了p2


上面的克隆确实没有任何问题,但是倘若我们克隆对象的类中,成员里有一个引用类型

那么克隆就会出现问题

class Target implements Cloneable{
    
    
    int num;
    int[] arr;

    public Target(int num, int[] arr) {
    
    
        this.num = num;
        this.arr = arr;
    }

    public Object clone() throws CloneNotSupportedException {
    
    
        return super.clone();
    }
}
public class Test {
    
    
    public static void main(String[] args) throws CloneNotSupportedException {
    
    
        Target p1 = new Target(100,new int[]{
    
    1,2,3});
        Target p2 = (Target) p1.clone();
        p2.arr[0] = 3;
        System.out.println(p1.arr[0]);
    }
}
//依次打印 3 3

很明显我们这里是想要只改变p2中的arr[0]但是此时arr[0]中的数字却也一起变了

那么是为什么呢,我们通过一张图来理解

在这里插入描述

可以发现,倘若我们克隆对象的类中,成员里有一个引用类型,如果这时我们直接将这个对象里面的内容进行克隆,对于引用类型来说我们克隆的也就是地址而已,而这个对象内部引用指向的内容我们并没有进行克隆。同时由于只克隆了地址,所以我们在操作克隆对象的引用类型成员的时候,操作的实际上都是同一块内容

那么上面的这种拷贝方式,就被称作为浅拷贝

而要想达到我们的预期效果,我们就要进行深拷贝,也就是将内部引用类型成员指向的内容也进行克隆,那么我们就要修改一下我们之前的克隆方法,不能直接返回super.clone()

public Object clone() throws CloneNotSupportedException {
    
    
    //先创建一个临时变量,存储浅拷贝数据
    Target tmp = (Target) super.clone();
    //用成员内的引用类型调用拷贝方法,将它指向的内容也进行拷贝
    //并且将拷贝出的内容的地址放入临时变量的对应位置
    tmp.arr = this.arr.clone();
    //返回完成拷贝的数据
    return tmp;
}

注意:如果成员内的引用类型是自定义类型,则要在那个引用类型里实现Cloneable接口,重写clone()方法,并且在将克隆结果传入tmp的时候要进行向下转型

那么此时我们运行下面代码的时候,会发现上面的问题就成功解决了

class Target implements Cloneable{
    
    
    int num;
    int[] arr;

    public Target(int num, int[] arr) {
    
    
        this.num = num;
        this.arr = arr;
    }

    public Object clone() throws CloneNotSupportedException {
    
    
        Target tmp = (Target) super.clone();
        tmp.arr = this.arr.clone();
        return tmp;
    }
}
public class Test {
    
    
    public static void main(String[] args) throws CloneNotSupportedException {
    
    
        Target p1 = new Target(100,new int[]{
    
    1,2,3});
        Target p2 = (Target) p1.clone();
        p2.arr[0] = 3;
        System.out.println(p1.arr[0]);
        System.out.println(p2.arr[0]);
    }
}

内部类

内部类的概念

顾名思义,内部类就是处于内部的类,但是处在什么地方的内部呢?

内部类可以分为四种类型:普通内部类、实例内部类、静态内部类、匿名内部类

实际上这四种类都会处于类中,但是倘若要进行细致的划分的话,普通内部类和匿名内部类是处于方法中或者代码块中的而实例内部类和静态内部类是处于类内但是在方法外的

用变量的生命周期来解释的话,普通内部类和匿名内部类的生命周期就是局部变量的生命周期,而实例代码块的生命周期就是实例变量的生命周期,静态代码块的生命周期就是静态变量的生命周期

下面我们就来分别了解一下这几个内部类

普通内部类

定义在局部区域内的类就被称作局部内部类

public class Test {
    
    
    public static void test(){
    
    
        class InnerClass{
    
    
            public void print(){
    
    
                System.out.println("hehe");
            }
        }
        //正常运行
        InnerClass ic = new InnerClass();
        ic.print();
    }
    
    
    public static void main(String[] args) {
    
    
        //报错
        InnerClass ic = new InnerClass();
        ic.print();
    }
}

这个类用的不是很多,因此不多阐述,了解即可

实例内部类

实例内部类,也被称作成员内部类。顾名思义,就是定义一个类作为一个类的成员

那么假如我们想要实例化实例内部类,应该怎么做呢?先看一个例子

class OuterClass{
    
    
    int outerNum = 10;
    class InnerClass{
    
    
        int innerNum = 20;
    }
}

public class Test {
    
    
    public static void main(String[] args) {
    
    
        //报错 无法识别InnerClass
        InnerClass innerClass = new InnerClass();
    }
}

此时我们在外部直接用new InnerClass()是不行的,因为这个内部类是OuterClass的成员,如果OuterClass没有实例化,这个InnerClass()都不存在

因此我们应该先实例化一个OuterClass对象,但是我们上面也说过,这个内部类是OuterClass的成员,参考初始化成员方法,我们直接去实例化InnerClass()也是不可能的,要实例化InnerClass()应该借助OuterClass对象的引用

那么应该如何书写呢?格式如下

OuterClass outerClass = new OuterClass();
OuterClass.InnerClass innerClass = outerClass.new InnerClass();

这个类型是属于外部类的成员,因此类型是OuterClass.InnerClass,同时借助OuterClass对象的引用来new一个InnerClass对象

那么在例子中,代码如下

class OuterClass{
    
    
    int outerNum = 10;
    class InnerClass{
    
    
        int innerNum = 20;
    }
}

public class Test {
    
    
    public static void main(String[] args) {
    
    
        OuterClass outerClass = new OuterClass();
        OuterClass.InnerClass innerClass = outerClass.new InnerClass();
    }
}

那么接下来我们再来看看实例内部类对于外部类和其自身的成员访问

假设没有重名的情况下,所有的成员变量是可以直接访问的

class OuterClass{
    
    
    int outerNum = 10;
    class InnerClass{
    
    
        int innerNum = 20;
        public void print(){
    
    
            System.out.println(innerNum);
            System.out.println(outerNum);
        }
    }
}

public class Test {
    
    
    public static void main(String[] args) {
    
    
        OuterClass outerClass = new OuterClass();
        OuterClass.InnerClass innerClass = outerClass.new InnerClass();
        innerClass.print();
    }
}
//依次打印 20 10

重名情况下,优先访问实例内部类成员,因为默认为访问成员为this前缀,如果找不到会再尝试外部类类名.this访问

class OuterClass{
    
    
    int test = 10;
    class InnerClass{
    
    
        int test = 20;
        public void print(){
    
    
            System.out.println(test);
        }
    }
}


public class Test {
    
    
    public static void main(String[] args) {
    
    
        OuterClass outerClass = new OuterClass();
        OuterClass.InnerClass innerClass = outerClass.new InnerClass();
        innerClass.print();
    }
}
//打印20

因此重名的情况下如果想要访问外部类成员,则要通过外部类类名.this访问

class OuterClass{
    
    
    int test = 10;
    class InnerClass{
    
    
        int test = 20;
        public void print(){
    
    
            System.out.println(OuterClass.this.test);
        }
    }
}


public class Test {
    
    
    public static void main(String[] args) {
    
    
        OuterClass outerClass = new OuterClass();
        OuterClass.InnerClass innerClass = outerClass.new InnerClass();
        innerClass.print();
    }
}

我们之前说过,this实际上是成员方法的一个隐藏参数,代表着调用这个方法的对象的引用,那么在实例内部类里面,还会隐藏一个指向外部类对象的this引用参数,那么我们就可以通过这个引用来访问外部类成员

一些其他注意事项:

  1. 实例内部类也是成员,也受到访问修饰符的限制
  2. 外部类中,不能直接访问实例内部类中的成员,如果要访问必须先要创建内部类的对象,因为没有实例化的时候,内部类成员不存在

这里要注意,在Java8环境中,不支持在实例内部类里面声明静态变量

class OuterClass{
    
    
    int test = 10;
    class InnerClass{
    
    
        //报错,在Java8环境中,不支持在实例内部类里面声明静态变量
        static int test = 20;
    }
}

那么此时解决方案有两个:

  1. 让这个静态变量变为静态常量,即加上final前缀
  2. 把内部类类型变为静态内部类

静态内部类

和其他静态成员一样,静态内部类也是属于类的。创建方式也很简单,在实例内部类前面加上static即可

那么假如我们想要实例化静态内部类,应该怎么做呢?先看一个例子

class OuterClass{
    
    
    static int outerNum;
    static class InnerClass{
    
    
    }
}

public class Test {
    
    
    public static void main(String[] args) {
    
    
        OuterClass.outerNum = 1;
        System.out.println(OuterClass.outerNum);
    }
}
//打印 1

普通的实例化方法肯定不用试了,那我们不妨参考一下初始化静态变量的方法

可以看到,因为静态成员是属于类的,所以我们是可以直接通过类名去初始化静态变量的

那么静态内部类应该也同理,我们类型依旧是OuterClass.InnerClass,因为你即使变成了静态成员,你的类型也不会变的。然后就是new的方式,由于不依赖于对象,我们就不用通过对象引用去new一个InnerClass,而是可以像创建一个普通类一样直接new一个OuterClass.InnerClass

class OuterClass{
    
    
    
    static class InnerClass{
    
    
    }
}

public class Test {
    
    
    public static void main(String[] args) {
    
    
        //创建一个静态内部类对象
        OuterClass.InnerClass innerClass = new OuterClass.InnerClass();
    }
}

与静态变量不同的一点是,我们如果还是想要通过对象引用初始化静态成员变量的话,也是可以的

但是,外部类对象引用不能用来实例化静态内部类对象

class OuterClass{
    
    
    static int outerNum;
    static class InnerClass{
    
    
    }
}

public class Test {
    
    
    public static void main(String[] args) {
    
    
        OuterClass outerClass = new OuterClass();
        //只会报警告
        outerClass.outerNum = 10;
        
        //编译不通过
        OuterClass.InnerClass innerClass = outerClass.new InnerClass();
    }
}

我们上面讲到内部类的静态成员,由于静态成员都是属于类的,因此静态内部类是属于外部类的,里面的静态变量是属于静态内部类的,那么这样我们甚至可以在外部类对象不存在的时候就直接访问

class OuterClass{
    
    
    static int outerNum = 10;
    static class InnerClass{
    
    
        static int InnerNum = 20;
    }
}

public class Test {
    
    
    public static void main(String[] args) {
    
    
        System.out.println(OuterClass.outerNum);
        System.out.println(OuterClass.InnerClass.InnerNum);
    }
}
//依次打印 10 20

一些其他注意事项:

  1. 静态内部类也是成员,也受到访问修饰符的限制
  2. 如果要在静态内部类里面访问外部类的非静态成员,则只能通过创建外部类对象后通过对象访问

匿名内部类

匿名内部类,顾名思义,没有名字的类,那么没有名字的类应该怎么创建并且实例化呢?

通常情况下,匿名内部类通过直接继承类或者实现接口来创建,并且实例化对象会在创建的同时进行

由例子说明

abstract class Animal{
    
    
    public abstract void eat();
}
interface Eating{
    
    
    void eat();
}
public class Test {
    
    
    public static void main(String[] args) {
    
    
        
        Animal animal = new Animal() {
    
    
            @Override
            public void eat() {
    
    
                System.out.println("吃东西");
            }
        };
        
        Eating eating = new Eating() {
    
    
            @Override
            public void eat() {
    
    
                System.out.println("吃东西");
            }
        };
        
    }
}

根据我们以前的知识,我们知道接口和抽象类是不能实例化的,因此这里看着像是实例化了接口和抽象类,但实际上是创建了两个匿名内部类,分别继承了Animal类和实现了Eating接口,并且同时直接实例化了匿名内部类的对象

内部类的字节码文件

内部类虽然和外部类在同一个.java源文件里面,但是被编译后产生的字节码文件却是分开的,我们以下面代码为例,看一看编译后的字节码文件

abstract class Animal{
    
    
    public abstract void eat();
}
interface Eating{
    
    
    void eat();
}
public class Test {
    
    
    public static class StaticInnerClass{
    
    
        //静态内部类
        static int num = 10;
    }
    public class InnerClass{
    
    
        //实例内部类
         int num = 10;
    }
    public static void main(String[] args) {
    
    
        Animal animal = new Animal() {
    
    
            //匿名内部类1
            @Override
            public void eat() {
    
    
                System.out.println("吃东西");
            }
        };

        Eating eating = new Eating() {
    
    
            //匿名内部类2
            @Override
            public void eat() {
    
    
                System.out.println("吃东西");
            }
        };
    }
}

对应的文件名如下

猜你喜欢

转载自blog.csdn.net/qq_42150700/article/details/130643402