Java学习(10)多接口、接口的继承、抽象类和接口的区别、Object类【toString 、equals、hashcode】、接口实例 【compareTo、clone 】、浅拷贝和深拷贝、内部类

接上次博客:Java学习(9)(3种向上转型的方式、重写、向下转型、多态的优缺点、抽象类【基础规则、抽象类的作用】接口【基础规则、 接口的使用】)_di-Dora的博客-CSDN博客

目录

 接口中的继承

抽象类和接口的区别:

Object类

toString ( ) 方法

 对象比较equals方法

hashcode方法

​编辑 接口实例

 1、Comparator接口和compareTo方法

 2、克隆方法 clone()

浅拷贝和深拷贝

内部类

实例内部类

静态内部类

局部内部类

匿名内部类


实现多个接口(多继承)

我们以“动物”作为父类,它按照动物的物种来分类可以有多个子类(这里以狗和鸟为例);

同时,“动物”按照“会游泳的”、“会飞的”、“会跑的”……又可以分为不同的类。

可是一个类只可以有一个父类(动物的共性——IS-A),此时就可以让这些子类分别实现多个接口(不同的特性),完美地体现了“多态”的魅力。

父类Animal

public abstract class Animal {
    public String name;
    public int age;

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

    public abstract void eat();
}

子类Dog

//注意,一定是先继承,再实现!!!不可以把extends Animal放到implements IRunning,ISwimming后面
//一个类只可以继承一个类
//一个类可以实现多个接口
public class Dog extends Animal implements IRunning,ISwimming {
    public Dog(String name,int age) {
        super(name,age);
    }

    @Override
    public void eat() {
        System.out.println(name+" 吃狗粮! ");
    }


    @Override
    public void run() {
        System.out.println("狗狗现在四脚着地,撒欢地跑步!");
    }

    @Override
    public void swim() {
        System.out.println("狗狗正在狗刨式游泳!");
    }
}

子类Bird

public class Bird extends Animal implements IRunning,IFlying{
    public Bird(String name, int age) {
        super(name, age);
    }

    @Override
    public void eat() {
        System.out.println(name +" 正在吃米虫!");
    }

    @Override
    public void fly() {
        System.out.println(name +" 小鸟扇动翅膀,呜呼!起飞!");
    }

    @Override
    public void run() {
        System.out.println(name +" 小鸟迈开两条小短腿,滑稽跑动!");
    }
}

接口IFlying

public  interface  IFlying{
    void fly();
}

接口IRunning

public interface IRunning {
    void run();
}

接口ISwimming

public interface ISwimming {
    void swim();
}

Robot

public class Robot implements IRunning{
    @Override
    public void run() {
        System.out.println(" 机器人在跑!好酷啊!");
    }
}

测试代码Tset

public class Test9 {

    public static void test1(Animal animal) {
        animal.eat();
    }

    public static void test2(IRunning iRunning) {
        iRunning.run();
    }
    public  static void test3(ISwimming iSwimming) {
        iSwimming.swim();
    }

    public static void test4(IFlying iFlying) {
        iFlying.fly();
    }

    public static void main(String[] args) {
        Bird bird = new Bird("小鸟",1);
        Dog dog = new Dog("小狗",10);
        test2(bird);
        test2(dog);
        System.out.println("=========");
        //test3(bird);  会报错,鸟不会游泳
        test3(dog);
        System.out.println("=========");
        test4(bird);
        //test4(dog);   会报错,狗不会飞
        System.out.println("=========");
        test2(new Robot());
        main1();
    }

    public static void main1() {
        Bird bird = new Bird("小鸟",1);
        Dog dog = new Dog("小狗",10);

        test1(bird);
        test1(dog);


    }
}

 

下面是实例的一些补充说明: 

现在还在报错,因为还没有实现方法的重写:

 生成重写方法的快捷键:鼠标放在Animal,ALT+ENTER

 

 接口中的继承

interface A {
    void testA();
}
interface B {
    void testB();
}
interface C {
    void testC();
}
//有一个接口  具备B和C接口的功能 extends拓展
interface D extends B,C{
    //D 这个接口 具备了 B和C 的功能
    //D 实现了自己的功能
    void testD();
}
public class Test implements D{
    //要去实现B、C、D 的功能

    @Override
    public void testB() {

    }

    @Override
    public void testC() {

    }

    @Override
    public void testD() {

    }
}

抽象类和接口的区别:

抽象类和接口是Java中两种重要的抽象概念,它们都是Java中多态的常见使用方式,都可以用来描述一组相关的方法。虽然它们都可以用于实现多态性和抽象性,但它们之间有一些重要的区别。

核心区别:抽象类中可以包含普通方法和普通字段,这样就可以实现代码的复用(不用重写),而接口中不可以包含普通方法,子类必须重写所有的抽象方法。

1、抽象类可以有构造函数,而接口不能。因为抽象类是类的一种,所以它可以有构造函数来初始化它的成员变量。然而,接口不是类,它只是一组抽象方法的集合,所以它不能有构造函数。

2、抽象类可以有非抽象方法(普通方法),而接口不能。抽象类可以包含非抽象的方法实现,这些方法可以被子类继承或覆盖。接口只能包含抽象方法,这些方法没有实现,必须由实现接口的类提供具体的实现。

3、类可以实现多个接口,但只能继承一个抽象类。Java中不支持多重继承,这意味着一个类只能继承一个抽象类。然而,一个类可以实现多个接口,这使得它可以具有多个不同的行为。

4、接口中的方法默认是 public 和 abstract 的(成员默认都是 public 、final、static 的全局常量),而抽象类中的方法可以有不同的访问修饰符和实现方式(成员也是如此)。接口中的方法默认是 public 和 abstract 的,因此它们必须由实现接口的类提供具体的实现。抽象类中的方法可以有不同的访问修饰符和实现方式,它可以包含抽象方法、非抽象方法和静态方法等。

5、抽象类可以有实例变量(普通字段),而接口不能。抽象类可以有实例变量,这些变量可以被子类继承或覆盖。然而,接口不能有实例变量,因为接口只是一组抽象方法的集合。

6、抽象类用于表示一般化的概念,而接口用于表示行为的规范。抽象类用于表示一般化的概念,它们通常具有一个抽象的概念,可以由具体的子类细化。接口用于表示行为的规范,它们通常描述一组类应该具有的行为,而不关心这些类的具体实现。

7、关键字不同,implements 关键字实现接口,extends 关键字实现继承。

8、接口不可以继承抽象类,可以继承多个父接口;抽象类可以实现若干接口。

总之,抽象类和接口都是Java中重要的抽象概念,它们各自有着不同的特性和用途。在使用时需要根据具体情况选择合适的方式。如果你需要一个可以继承的类并且它有一些默认的实现,那么你应该使用抽象类。如果你需要定义一组类应该具有的行为,那么你可以选择接口。

还要强调的是,抽象类存在的意义是为了让编译器更好的校验,如果创建了一个抽象类的实例,编译器就会及时提醒我们(想想吧,一只Animal长什么样?噫~~~)。

Object类

Object类是Java中所有类的父类,它是Java语言中最基本的类之一,其他所有的类都是直接或间接地从Object类继承而来的,即所有类的对象都可以使用Object的引用进行接收

什么意思?我们来具体说明一下:

现在,让我们假设你正在编写一个商店系统,其中有多种类型的商品,包括食品、电子产品、书籍等。你需要编写一个方法,该方法可以接收任何类型的商品,并计算出它们的总价格。

为了实现这个目标,你可以定义一个接收 Object 类型参数的方法。然后,你可以在方法内部使用类型转换将 obj 转换回其原始类型,并计算出每个商品的价格。下面是我们的示例代码:

public class Example {
    public static void main(String[] args) {
        // 创建不同类型的商品对象
        Food food = new Food("Apple", 1.5, 2.0);
        Electronics electronics = new Electronics("iPhone", 799.0);
        Book book = new Book("Java Programming", "John Doe", 39.99);

        // 创建一个商品列表
        List<Object> items = new ArrayList<>();
        items.add(food);
        items.add(electronics);
        items.add(book);

        // 计算商品的总价格
        double totalPrice = calculateTotalPrice(items);
        System.out.println("Total price: $" + totalPrice);
    }

    public static double calculateTotalPrice(List<Object> items) {
        double total = 0.0;
        for (Object obj : items) {
            // 使用类型转换将 obj 转换回其原始类型
            if (obj instanceof Food) {
                Food food = (Food) obj;
                total += food.getPrice();
            } else if (obj instanceof Electronics) {
                Electronics electronics = (Electronics) obj;
                total += electronics.getPrice();
            } else if (obj instanceof Book) {
                Book book = (Book) obj;
                total += book.getPrice();
            } else {
                // 处理未知类型的商品
                System.out.println("Unknown item: " + obj.toString());
            }
        }
        return total;
    }
}

// 定义不同类型的商品类
class Food {
    private String name;
    private double weight;
    private double price;

    public Food(String name, double weight, double price) {
        this.name = name;
        this.weight = weight;
        this.price = price;
    }

    public double getPrice() {
        return weight * price;
    }
}

class Electronics {
    private String name;
    private double price;

    public Electronics(String name, double price) {
        this.name = name;
        this.price = price;
    }

    public double getPrice() {
        return price;
    }
}

class Book {
    private String title;
    private String author;
    private double price;

    public Book(String title, String author, double price) {
        this.title = title;
        this.author = author;
        this.price = price;
    }

    public double getPrice() {
        return price;
    }
}

需要注意的是:虽然这种方法灵活,可以接受多种类型的对象,但也有一些缺点。由于它不知道传递的对象是什么类型,因此在方法内部进行处理时可能需要进行大量的类型检查和类型转换。此外,由于没有类型安全检查,因此在运行时可能会发生错误。因此,在实际编程中,你应该尽可能使用更具体的类型作为参数类型,以提高代码的可读性和可维护性。

好了,我们回到 Object 类,来谈谈它的一些方法。

它的方法其实蛮少的,我们所有的类都可以调用这些方法。

 如果你不满意它的方法实现,你可以在自己的类里面把这些方法重写一遍,比如:

toString ( ) 方法

打印对象中的内容,获取对象信息。

package demo3;

import java.util.Objects;

class Person {
    public String name;
    public int age;

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

        Person person1 = new Person("zhangsan",10);
        System.out.println(person1);
    }
}

我们知道,"System.out.println();" 会直接调用 toString() 方法,此时我们没有重写,会显示什么呢? 

显示了一个全路径。

向上转型: 

 重写后: 

向上转型,动态绑定

package demo3;

import java.util.Objects;

class Person {
    public String name;
    public int age;

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

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

    }
}
public class Test {
    public static void main(String[] args) {

        Person person1 = new Person("zhangsan",10);
        System.out.println(person1);
    }
}

 对象比较equals方法

class Person {
    public String name;
    public int age;

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

}

public class Test {
    public static void main(String[] args) {

        Person person1 = new Person("zhangsan",10);
        System.out.println(person1);
        Person person2 = new Person("zhangsan",10);
        System.out.println(person2);
        System.out.println("=======");

 
        //此时比较的就是变量中的值------可以认为是地址,此时地址是不一样的
        System.out.println(person1 == person2);

        System.out.println(person1.equals(person2));

        Person person3 = person1;
        System.out.println(person1.equals(person3));

    }
}

 我们来看看equals ( ) 方法

谁调用equals,谁就是 this 。 

我们这个时候可以知道,此时这两行代码实现的效果其实是没有区别的:

都是比较地址。

所以记住,以后自定义的类型,那么一定记住要重写我们的equals方法。

 System.out.println(person1 == person2);

 System.out.println(person1.equals(person2);

所以我们现在要来实现自己的equals方法


    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
/*        //如果不是person对象
          //和上面的 getClass() != o.getClass() 是等价的
        if(!(o instanceof Person))  return false;*/
        //向下转型,比较属性值
        Person person = (Person) o;
        return age == person.age && Objects.equals(name, person.name);
    }

hashcode方法

我们其实刚刚看到过这个方法,在toString方法里面:

它帮忙计算了一个具体的对象位置(这里涉及到了数据结构,比较复杂,所以我们暂且说它是一个内存地址) ,然后调用了Integer.toHexString ( ) 方法,将这个地址以16进制输出。

源代码:

这里的 native 代表它底层是由C\C++写的。

我们加两行代码到刚刚那个例子里:

public class Test {
    public static void main(String[] args) {

        Person person1 = new Person("zhangsan",10);
        System.out.println(person1);
        Person person2 = new Person("zhangsan",10);
        System.out.println(person2);
        System.out.println("=======");

        System.out.println(person1.hashCode());
        System.out.println(person2.hashCode());



        //此时比较的就是变量中的值
        System.out.println(person1 == person2);
        System.out.println(person1.equals(person2));

        Person person3 = person1;
        System.out.println(person1.equals(person3));

    }
}

但是我们发现这两个人的姓名和年龄是一样的,相当于同一个人,所以我们就可以重写这个方法,把这两个对象放到同一块地址: 

//后期讲到哈希表的时候 就知道  两个一样的对象我们想放在一个位置 此时就可以利用重写这个方法来做
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

 你也可以让Java自动生成这些方法:

 接口实例

 1、Comparator接口和compareTo方法

比较两个对象的大小

自定义类型 要想比较大小,一定要实现这个接口,要实现这个接口,就要顺便实现compareTo方法,以实现比较的逻辑。

package demo4;

import java.util.Arrays;
import java.util.Comparator;

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

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

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

/*
    @Override
    public int compareTo(Student o) {
        //return this.age - o.age;
        *//*if(this.age > o.age) {
            return 1;
        }else if(this.age < o.age) {
            return -1;
        }else {
            return 0;
        }*//*
        return this.age - o.age;
    }
}*/
    public int compareTo(Student o) {
        return this.age - o.age;
    }
}

//优点  对类的侵入性 不强
//这样写的话可以根据年龄和姓名比较
//比较器
class AgeComparator implements Comparator<Student> {
    @Override
    public int compare(Student o1, Student o2) {
        return o1.age - o2.age;
    }
}
class NameComparator implements Comparator<Student> {
    @Override
    public int compare(Student o1, Student o2) {
        return o1.name.compareTo( o2.name) ;
    }
}

public class Test {

    public static void main(String[] args) {
        Student student1 = new Student("zhangsan", 10);
        Student student2 = new Student("lisi", 15);
        //我们想要比较两个学生的大小,又不可以用
        //System.out.println(student1>student2);
        //这个时候就可以实现一个接口

        System.out.println(student1.compareTo(student2));
        //student1和student2这个对象比较

        AgeComparator ageComparator = new AgeComparator();
        System.out.println(ageComparator.compare(student1, student2));

        NameComparator nameComparator = new NameComparator();
        System.out.println(nameComparator.compare(student1, student2));
    }

}

 注意,name是一个String引用类型,不可以直接比较,我们也要调用它的compareTo方法:

或者我们建立一个数组,来实现排序:

package demo4;

import java.util.Arrays;
import java.util.Comparator;

class Student {
    public String name;
    public int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = 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",10);
        students[1] = new Student("lisi",2);
        students[2] = new Student("wangwu",18);

        Arrays.sort(students);

        System.out.println(Arrays.toString(students));


    }

}

 这里这样写是不行的。因为没有交代清楚排序的时候根据什么排序。

 它是需要把数组内容强转为Comparable的,但是我们现在没有实现!

package demo4;

import java.util.Arrays;
import java.util.Comparator;

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

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

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

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

 改回来,就可以了:

 我们来模拟实现一个冒泡排序:

public class Test {

 public static void bubbleSort(Comparable[] comparable) {
        for (int i = 0; i < comparable.length-1; i++) {
            for (int j = 0; j < comparable.length-1-i; j++) {
                if(comparable[j].compareTo(comparable[j+1]) > 0) {
                    Comparable tmp = comparable[j];
                    comparable[j] = comparable[j+1];
                    comparable[j+1] = tmp;
                }
            }
        }
    }


    public static void main(String[] args) {

        Student[] students = new Student[3];
        students[0] = new Student("zhangsan",10);
        students[1] = new Student("lisi",2);
        students[2] = new Student("wangwu",18);

        //Arrays.sort(students);
        bubbleSort(students);
        System.out.println(Arrays.toString(students));

    }

}

 2、克隆方法 clone()

首先,我们的Object类有这个方法,但是我们发现我们直接调用clone( ) 是调用不了的,问题出在访问权限!(protected:不同包的子类,通过super调用)

 所以我们需要重写这个方法。

package demo4;

class Person {
    public int age;


    public Person(int age) {
        this.age = age;

    }


    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

}
public class Test2 {
    public static void main(String[] args) {
        Person person1 = new Person(10);
        Person person2 = person1.clone();
}

可是这个时候有出现问题了……

抛出异常,clone方法的异常是受查异常/编译时异常,必须是编译时处理。

ALT+ENTER

package demo4;

class Person {
    public int age;


    public Person(int age) {
        this.age = age;

    }


    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

}
public class Test2 {
    public static void main(String[] args) throws CloneNotSupportedException{
        Person person1 = new Person(10);
        Person person2 = person1.clone();
}

等等,怎么还有错?返回值是一个object,需要向下转型: 

package demo4;

class Person {
    public int age;
 
    public Person(int age) {
        this.age = age;

    }


    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

}
public class Test2 {
    public static void main(String[] args) throws CloneNotSupportedException{
        Person person1 = new Person(10);
        Person person2 = (Person) person1.clone();
}

现在运行一下,又出现问题!

不支持克隆???

好吧,我们还需要实现一个接口:

class Person implements Cloneable {
    public int age;


    public Person(int age) {
        this.age = age;

    }

 这个接口没有任何东西!

它叫做“空接口”或者“标记接口”:证明当前类是可以被克隆的。

这样,我们的克隆就成功了!(撒花~)

package demo4;


    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
class Person implements Cloneable {
    public int age;

    public Person(int age) {
        this.age = age;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    @Override
    public String toString() {
        return "Person{" +
                " age=" + age +
                '}';
    }
}
public class Test2 {
    public static void main(String[] args) throws CloneNotSupportedException {
        Person person1 = new Person(10);
        Person person2 = (Person) person1.clone();
        System.out.println(person1);
        System.out.println(person2);

    }
}

 当然,上面那个克隆只是最简单的克隆。接下来,我们来深入聊聊克隆。

浅拷贝和深拷贝

我们对刚刚的代码做一点微小的变动:

package demo4;

class Money {
    public double money = 19.9;

}

class Person implements Cloneable {
    public int age;
    public Money m;

    public Person(int age) {
        this.age = age;
        this.m = new Money();
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    @Override
    public String toString() {
        return "Person{" +
                " age=" + age +
                '}';
    }
}
public class Test2 {
    public static void main(String[] args) throws CloneNotSupportedException {
        Person person1 = new Person(10);
        Person person2 = (Person) person1.clone();
        System.out.println(person1);
        System.out.println(person2);
        System.out.println(person1.m.money);
        System.out.println(person2.m.money);
        System.out.println("==========================");
        person2.m.money = 99.99;
        System.out.println(person1.m.money);
        System.out.println(person2.m.money);
    }
}

 上面这种情况叫做“浅拷贝”——对象里面的对象我们没有克隆。

那么我们怎么进行“深拷贝”呢?要将对象中的对象也进行克隆。

所以我们也需要在Money这个类里面重写clone ( ) 方法(注意,还要调用它才行;并且一定要记得强转!),并且实现接口:

package demo4;

class Money implements Cloneable{
    public double money = 19.9;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
class Person implements Cloneable {
    public int age;
    public Money m;

    public Person(int age) {
        this.age = age;
        this.m = new Money();
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        //return super.clone();
        Person tmp = (Person) super.clone();
        tmp.m = (Money) this.m.clone();
        return tmp;
    }

    @Override
    public String toString() {
        return "Person{" +
                " age=" + age +
                '}';
    }
}
public class Test2 {
    public static void main(String[] args) throws CloneNotSupportedException {
        Person person1 = new Person(10);
        Person person2 = (Person) person1.clone();
        System.out.println(person1);
        System.out.println(person2);
        System.out.println(person1.m.money);
        System.out.println(person2.m.money);
        System.out.println("==========================");
        person2.m.money = 99.99;
        System.out.println(person1.m.money);
        System.out.println(person2.m.money);
    }
}

 

这样就没有问题了!

现在我们就可以对“深拷贝”和“浅拷贝”进行一下简单的总结了,毕竟,了解清楚概念还是蛮重要的:

深拷贝(deep copy)和浅拷贝(shallow copy)是计算机编程中常用的两种数据拷贝方式(代码的实现不同)。

浅拷贝是指创建一个新的对象,但是只复制原始对象的基本数据类型的值和内存地址,而不会复制原始对象中的引用类型数据的内存地址。这意味着,如果修改新对象中的引用类型数据,会影响到原始对象中的引用类型数据。在大多数编程语言中,浅拷贝可以通过赋值操作或者调用对象的浅拷贝方法来完成。

深拷贝则是将原始对象中的所有数据类型都复制一份到新的对象中,包括原始对象中的引用类型数据。这样,新对象中的数据和原始对象完全独立,修改新对象中的任何数据都不会影响原始对象中的数据(就像是两个独立的对象一样)。在大多数编程语言中,深拷贝可以通过调用对象的深拷贝方法来完成,或者利用一些第三方库来实现。

需要注意的是,深拷贝的性能比浅拷贝要差,因为它需要遍历原始对象中的所有数据类型,而且如果原始对象中的数据结构非常复杂,深拷贝的时间和空间复杂度都会比较高。因此,在实际编程中,需要根据具体情况选择合适的拷贝方式。

内部类

我们今天先来简单了解一下内部类的概念: 

当一个事物的内部,有一个部分需要一个完整的结构进行描述,而这个内部的完整的结构又只为外部事物提供服务,那么这个内部的完整结构最好使用内部类。Java中,将一个类定义在另一个类的内部或者定义在一个方法的内部叫做内部类。

内部类也是封装的一种体现。

一个内部类一个字节码文件:

我们打开这个代码所存储的位置看看;

正如我们之前提到过的,在面向对象编程中,外部类是指定义在一个包中或者独立的一个源文件中的类。

外部类可以包含成员变量、方法、构造函数和内部类等成员。

外部类的成员可以被外部类的对象或其他类的对象所访问,但是外部类不能直接访问内部类的成员,需要通过内部类的对象来访问。

在Java语言中,一个源文件只能定义一个公共的外部类,这个类的名称必须与源文件名相同。其他非公共的外部类可以定义在同一个源文件中,但是不能被其他包中的类访问。外部类可以用public、protected、private或者默认访问修饰符来控制成员的访问范围。

内部类可以访问包含它的外部类的所有成员变量和方法,包括私有成员变量和方法,同时外部类也可以访问内部类的成员变量和方法。在Java语言中,内部类包括成员内部类、静态内部类、局部内部类和匿名内部类。

1、成员内部类(实例内部类)(Member Inner Class):定义在另一个类的内部,并且不是静态的。成员内部类可以访问外部类的成员变量和方法,并且可以包含静态变量、常量、方法、构造函数等。

2、静态内部类(Static Inner Class):也是定义在另一个类的内部,但是必须是静态的。静态内部类不能访问外部类的非静态成员变量和方法,但可以访问外部类的静态成员变量和方法。

3、局部内部类(普通内部类)(Local Inner Class):定义在方法内部的类,只能在定义它的方法中访问。局部内部类可以访问外部类的成员变量和方法,但只能访问方法中的final变量。

局部内部类可以访问外部类的成员变量和方法,但只能访问方法中的final变量。这意味着在局部内部类中,你可以直接访问外部类的成员变量和方法,而不需要通过外部类的实例。但是,如果你想访问方法中的非 final 变量,编译器将会报错,因为非 final 变量的值在方法执行结束后可能会改变,而局部内部类的实例仍然可能存在。

局部内部类通常用于需要定义一些特定实现的接口或抽象类的情况,或者需要在方法中定义一些辅助类的情况。由于局部内部类的作用域被限制在方法内部,因此它们对于封装实现细节和避免命名冲突非常有用。

现实中我们很少使用它。

4、匿名内部类(Anonymous Inner Class):没有类名的内部类,通常用于定义一次性的、简单的类。匿名内部类可以继承一个类或实现一个接口,可以访问外部类的成员变量和方法,但是不能定义构造函数。

class OuterClass {

    //一个类一个字节码文件
    class InnerClass {
        //实例内部类
    }

    static class InnerClass2{
        //静态内部类
    }

}

//写一个接口A
interface A {
    void testA();
}
public class Test {
    public static void main(String[] args) {
        //平时直接  new A; 可以吗?
        //当然不可以,但是我们可以这么做:
        //以下代码可以认为:有一个类 实现了A接口并且 重写了A接口中的方法
        new A () {
            @Override
            public void testA () {
                System.out.println("嘻嘻!");
            }
        }.testA();  //我们可以这样调用它
        //或者:
        //以下代码可以认为:有一个类 实现了A接口并且 重写了A接口中的方法
        A a = new A(){
            @Override
            public void testA() {
                System.out.println("哈哈!");
            }
        };//匿名内部类:这个类没有名字
        a.testA();
    }
}

内部类的使用可以使代码更加简洁和可读,同时可以有效地隐藏实现细节,提高程序的安全性和可维护性。但是,由于内部类会增加类的嵌套层数,同时内部类的使用也会影响程序的性能,因此在实际开发中需要谨慎使用。

刚刚我们对内部类已经有了一个大概的认识了,接下来让我们深入了解一下:

实例内部类

实例内部类的语法规则和注意事项:

1.外部类中的任何成员都可以在实例内部类方法中直接访问。即实例内部类可以访问外部类的私有成员,因为它们属于同一个类。实例内部类可以访问外部类的静态成员和方法。

2.实例内部类所处的位置与外部类成员位置相同,因此也受public、private等访问限定符的约束。

3.在实例内部类方法中访问同名的成员时,优先访问内部类自己的,如果要访问外部类同名的成员,必须使用"外部类名称.this.同名成员"来访问。

这里就引申出一个问题,在这个例子里,我们怎么拿到外部类的数据呢?

用 this?可是这里就是InnerClass调用的方法啊,取得的还是内部类的成员。

应该这样:OuterClass.this.data1。

class OuterClass {
    //
    public int data1 = 1;
    private int data2 = 2;
    public static int data3 = 3;


     //当外部类中数据成员 和 内部类中的数据成员一样的时候: OuterClass.this.
     //实例内部类当中 是包含 外部类的this的
     

    class InnerClass {
        public int data1 = 1111;
        public int data4 = 4;
        private int data5 = 5;

        public static final int data6 = 6;

        public void test() {
            System.out.println(this.data1);// 打印的是内部类的data1
            System.out.println(OuterClass.this.data1);// 打印的是外部类的data1
            System.out.println(data4);
            System.out.println(data5);
            System.out.println("内部类的test方法");
        }
    }

    public void test() {
        System.out.println("外部类的test方法");
        InnerClass innerClass = new InnerClass();
        System.out.println(innerClass.data4);
    }
}



public class Test2 {
    public static void main(String[] args) {
        //InnerClass innerClass = new InnerClass();//这样是错误的!
        OuterClass outerClass = new OuterClass();

        //获取实例内部类对象的时候 依赖于外部类对象
        OuterClass.InnerClass innerClass =  outerClass.new InnerClass();
        innerClass.test();

        OuterClass.InnerClass innerClass2 =  new OuterClass().new InnerClass();
        //innerClass2.test();

    }
}

4.实例内部类对象必须在先有外部类对象前提下才能创建。即,先要有一个外部类对象,才可以有一个内部类对象!获取实例内部类对象的时候,依赖于外部类对象!获取实例内部类对象需要先创建外部类的实例,然后通过该实例来创建内部类的对象。

5.实例内部类的非静态方法中包含了一个指向外部类对象的引用。

在Java中,实例内部类是定义在另一个类的内部的类,它可以访问外部类的实例变量和方法,就像它们是自己的一样。这是因为实例内部类的非静态方法中包含了一个指向外部类对象的引用,这个引用可以让实例内部类访问外部类的实例变量和方法。

具体来说,当外部类创建一个实例内部类的对象时,实例内部类的构造函数会自动接收一个指向外部类对象的引用,这个引用被保存在实例内部类的成员变量中。这个引用可以在实例内部类的非静态方法中使用,以访问外部类的实例变量和方法。

例如,假设有一个外部类叫做Outer,它有一个实例变量叫做outerVar,还有一个非静态方法叫做outerMethod。Outer类中定义了一个实例内部类叫做Inner,它有一个非静态方法叫做innerMethod。Inner类中包含了一个指向外部类对象的引用,这个引用可以用来访问外部类的实例变量和方法。

以下是一个简单的示例代码:

public class Outer {
    private int outerVar;

    public void outerMethod() {
        Inner inner = new Inner();
        inner.innerMethod();
    }

    public class Inner {
        public void innerMethod() {
            System.out.println(outerVar);
            outerMethod();
        }
    }
}

在这个示例中,当Outer类的outerMethod方法被调用时,它创建了一个Inner对象,并调用了Inner对象的innerMethod方法。在innerMethod方法中,可以使用outerVar访问外部类的实例变量,也可以使用outerMethod访问外部类的实例方法。

总之,实例内部类的非静态方法中包含了一个指向外部类对象的引用,这个引用可以让实例内部类访问外部类的实例变量和方法。

在实例内部类的静态方法中不可以访问外部类的实例变量和方法吗? 

实例内部类的静态方法无法直接访问外部类的实例变量和方法。

静态方法是属于类的而不是对象的,因此它们不能访问任何特定对象的状态或行为。

其实这个地方很容易理解,它就和正常的类中的静态方法是一样的。

如果需要在实例内部类的静态方法中访问外部类的实例变量和方法,可以将外部类的实例作为参数传递给静态方法,然后通过这个实例来访问外部类的实例变量和方法。例如:

public class OuterClass {
    private int x;

    public void outerMethod() {
        System.out.println("Outer method");
    }

    public class InnerClass {
        public static void innerMethod(OuterClass outer) {
            System.out.println("Inner method with x = " + outer.x);
            outer.outerMethod();
        }
    }
}

在上面的例子中,InnerClass.innerMethod() 是一个静态方法,它需要一个 OuterClass 的实例作为参数,才能访问 OuterClass 的实例变量 x 和实例方法 outerMethod()。

6.外部类中,不能直接访问实例内部类中的成员,如果要访问必须先要创建内部类的对象。而且如果实例内部类中的成员是私有的,外部类中也无法访问。

7.实例内部类可以继承其他类或实现接口。实例内部类常用于实现某些特定的接口或者继承某个类的功能,从而实现更加灵活的代码设计和组织。

下面是一个基础的示例代码:

public class OuterClass {
    private int outerField;
    
    public class InnerClass {
          private int innerField;
        
          public InnerClass(int innerField) {
              //构造函数
              this.innerField = innerField;
          }
        
          public int getInnerField() {
              return innerField;
          }
     }
    
      public OuterClass(int outerField) {
        this.outerField = outerField;
      }
    
      public InnerClass createInnerObject(int innerField) {
          return new InnerClass(innerField);
      }
}

等一下!上面这里是不是有问题?

不是说实例内部类中的成员是私有的,外部类中也无法访问吗?

实例内部类中的成员确实是私有的,外部类不能直接访问

但是,外部类可以通过公共方法来创建和操作实例内部类的对象,这些公共方法可以在外部类中定义,以便外部类可以访问实例内部类的成员。

在如上代码中,OuterClass 中的 createInnerObject() 方法允许外部类创建并返回 InnerClass 对象,因此外部类是可以操作实例内部类的对象的。

所以这段代码是正确的。在外部类中定义公共方法来操作内部类的对象,是一种常见的处理实例内部类的方式。

如何获取实例内部类对象?

在上面的代码中,OuterClass包含一个内部类InnerClass。要创建一个InnerClass的实例,需要先创建一个OuterClass的实例,然后使用它来创建InnerClass的实例。例如:

OuterClass outer = new OuterClass(10);
OuterClass.InnerClass inner = outer.createInnerObject(20);
System.out.println("outerField: " + outer.outerField); // 输出 "outerField: 10"
System.out.println("innerField: " + inner.getInnerField()); // 输出 "innerField: 20"

在上面的示例代码中,我们首先创建了一个OuterClass的实例outer,并传入了参数10。然后,我们通过outer调用createInnerObject方法来创建一个InnerClass的实例inner,并传入参数20。最后,我们可以使用outer访问OuterClass的私有成员变量outerField,使用inner访问InnerClass的私有成员变量innerField。

也可以这样:

OuterClass.InnerClass inner = new OuterClass(10).createInnerObject(20);

注意:在实例内部类中不可以定义一个 static 静态成员变量,除非加上一个 final ,为什么?

在实例内部类中定义静态成员变量需要考虑到内部类的生命周期和作用域。由于实例内部类的生命周期和静态成员变量的作用域存在矛盾,因此在实例内部类中定义静态成员变量是有限制的。

首先,实例内部类是依附于外部类的实例存在的,它的生命周期也受到外部类实例的限制。当外部类实例被销毁时,与之关联的实例内部类也会被销毁。

其次,静态成员变量是属于类的,不属于对象实例,因此在类加载时就已经存在,并且可以在类的所有实例中共享。而实例内部类的定义是在外部类实例中的,实例内部类的静态成员变量也只能在实例内部类所属的外部类实例中共享。

这里具体解释一下:

实例内部类的定义和静态成员变量的初始化方式不同,导致在实例内部类中定义静态成员变量会出现问题。

具体来说,类加载的时候不会加载普通的成员变量,实例内部类是在外部类的实例化过程中才被创建出来的,每个实例内部类的定义都依赖于外部类实例,而静态成员变量则是在类加载时就已经被初始化,不依赖于任何外部类实例。因此,如果在实例内部类中定义静态成员变量,这个静态成员变量就无法在实例化过程之前被初始化,因为此时类还没有被加载,也就没有静态成员变量可以使用。

更进一步地说,即使在实例化过程中静态成员变量能够被初始化,由于每个实例内部类的定义都依赖于外部类实例,所以在不同的外部类实例中,这个静态成员变量的值也会不同,导致无法实现在实例内部类中共享数据的目的。

因此,为了避免这种问题,Java编译器禁止在实例内部类中定义静态成员变量。

如果要在实例内部类中定义静态成员变量,需要加上一个 final 关键字,表示这个静态变量是一个常量,而不是一个可变的静态变量。这样可以保证这个静态变量在实例内部类中只能被赋值一次,并且不能再修改。

总之,在实例内部类中定义静态成员变量需要仔细考虑其生命周期和作用域,以及是否需要加上 final 关键字来表示这个静态变量是一个常量。

就像这样:

class OuterClass {
    public int data1=1;
    private int data2=2;

    //实例内部类
    class InnerClass {

        public int data4=4;
        private int data5=5;
        public static final int data6=6;
        public void test(){
            System.out.println(data4);
            System.out.println(data5);
            System.out.println("内部类的test方法");
        }


    }
    public void test(){
        System.out.println(data1);
        System.out.println(data2);
        System.out.println(data3);
        System.out.println("外部类的test方法");
    }

}

静态内部类

静态内部类是指在一个类的内部定义的另一个类,且被定义的类被声明为静态。静态内部类与普通内部类的区别在于,静态内部类不会持有对外部类实例的引用,因此可以在没有外部类实例的情况下创建它的对象。

下面是关于静态内部类的注意事项和语法规则:

1.静态内部类的访问修饰符可以是public、protected、默认和private。

2.静态内部类只能直接访问外部类的静态成员,包括静态成员变量和静态方法。与实例内部类不同的是,静态内部类不依赖于外部类的实例,因此它不能直接访问外部类的非静态成员,包括实例变量和实例方法。但是,静态内部类可以访问外部类的静态成员和方法,因为它们属于外部类的类级别,不依赖于外部类的对象。

3.静态内部类可以包含静态成员变量和静态方法,这些成员变量和方法只能被静态内部类和外部类访问。

4.在外部类中,可以使用类名访问静态内部类的成员变量和方法。例如,如果静态内部类名为InnerClass,可以使用OuterClass.InnerClass.staticField来访问静态内部类的静态成员变量。

5.在外部类中,可以使用new OuterClass.InnerClass()来创建静态内部类的实例。

6.静态内部类可以包含自己的非静态方法和成员。静态内部类是一个静态成员,它与其他静态成员一样,可以包含静态和非静态成员和方法。

下面是一个简单的示例代码,展示了如何定义和使用静态内部类:

public class OuterClass {
    private static int outerStaticField = 1;
    private int outerField = 2;

    public static class InnerClass {
        private static int innerStaticField = 3;
        private int innerField = 4;

        public static void innerStaticMethod() {
            System.out.println("Inner static method");
        }

        public void innerMethod() {
            System.out.println("Inner method");
        }

        public void printFields() {
            System.out.println("Outer static field: " + outerStaticField); // 可以访问外部类的静态成员
            System.out.println("Inner static field: " + innerStaticField); // 可以访问内部类的静态成员
            System.out.println("Outer field: " + outerField); // 编译错误,不能访问外部类的非静态成员
            System.out.println("Inner field: " + innerField); // 可以访问内部类的非静态成员
        }
    }


    public static void main(String[] args) {
        OuterClass.InnerClass.innerStaticMethod(); // 可以通过类名访问静态内部类的静态方法
        OuterClass.InnerClass inner = new OuterClass.InnerClass(); // 可以通过类名创建静态内部类的实例
        inner.innerMethod(); // 可以通过静态内部类的实例访问内部类自己的非静态成员
        inner.printFields();
    }
}

那么如何在静态内部类当中,间接访问外部类的非静态的数据成员?

我们可以通过创建外部类的对象来访问。

例如,假设有一个外部类OuterClass和一个静态内部类StaticInnerClass,其中OuterClass有一个非静态成员变量nonStaticVar和一个静态成员变量staticVar:

public class OuterClass {
    private int nonStaticVar;
    private static int staticVar;

    public static class StaticInnerClass {
        public void accessOuterClassMember() {
            // 不能直接访问外部类的非静态成员nonStaticVar
            // 可以通过创建OuterClass对象来访问nonStaticVar
            OuterClass outerObj = new OuterClass();
            int nonStaticVarValue = outerObj.nonStaticVar;
            System.out.println("Non-static variable value: " + nonStaticVarValue);

            // 可以直接访问外部类的静态成员staticVar
            int staticVarValue = staticVar;
            System.out.println("Static variable value: " + staticVarValue);
        }
    }
}

在上面的示例中,StaticInnerClass中的accessOuterClassMember方法通过创建OuterClass对象outerObj来间接访问nonStaticVar,同时,我们也可以直接访问staticVar。 

局部内部类

局部内部类是指在一个方法或者代码块内部定义的类。它的作用域仅限于该方法或代码块内部,不能被外部类或其他方法访问。

下面是关于局部内部类的语法规则和注意事项:

1.局部内部类可以访问外部类的所有成员,包括私有成员。

2.局部内部类中不能定义静态成员变量和静态方法,因为局部内部类必须在方法或代码块内部定义,不能被声明为静态。

3.局部内部类中可以定义非静态成员变量和方法,但是不能使用public、protected、private修饰符,因为它的作用域仅限于该方法或代码块内部。

4.局部内部类可以访问外部类的final类型的局部变量,但是该变量在定义时必须被声明为final。如果在定义时未声明为final,编译器会报错。

局部内部类可以访问外部类的所有成员,包括私有成员,但是访问外部类的局部变量有一些限制。如果要访问外部类的局部变量,必须将其声明为final类型。如果在定义时未声明为final,则编译器会报错。

这个限制是由Java编译器实现的,其原因是当方法执行完毕后,方法的局部变量就会被销毁,而局部内部类对象可能在外部类方法返回后仍然存在,所以访问非final局部变量可能会引起不可预知的行为,因为这些变量的值可能在局部内部类对象被创建后发生改变。通过强制要求局部变量为final类型,编译器可以保证局部变量在局部内部类对象被创建时具有固定的值,从而避免这种不可预知的行为。

接下来,我用代码举个局部内部类访问外部类的局部变量例子:

public class OuterClass {
    public void outerMethod() {
        final int outerVar = 10;

        // 定义一个局部内部类
        class InnerClass {
            public void innerMethod() {
                // 访问外部类的局部变量
                System.out.println("Outer variable value: " + outerVar);
            }
        }

        InnerClass inner = new InnerClass();
        inner.innerMethod();
    }
}

在这个例子中,outerMethod() 方法包含了一个局部内部类 InnerClass。该内部类定义在 outerMethod() 方法内部,因此它可以访问 outerMethod() 方法内的所有变量,包括 outerVar。在 InnerClass 的 innerMethod() 方法中,我们使用 outerVar 变量,打印出了它的值。最后,我们在 outerMethod() 方法中创建了一个 InnerClass 的实例,并调用了它的 innerMethod() 方法,这样就实现了局部内部类访问外部类的局部变量的效果。

5.在外部类的方法中,可以通过类名创建局部内部类的实例,但是必须在方法或代码块内部定义之后才能创建。这是因为局部内部类必须在方法或代码块内部定义,否则就失去了其特殊的作用域。

下面是一个简单的示例代码,展示了如何定义和使用局部内部类:

public class OuterClass {
    private int outerField = 1;

    public void outerMethod() {
        final int finalVariable = 2; // 声明为final类型的局部变量

        class InnerClass {
            private int innerField = 3;

            public void innerMethod() {
                System.out.println("Outer field: " + outerField); // 可以访问外部类的成员变量
                System.out.println("Final variable: " + finalVariable); // 可以访问final类型的局部变量
                System.out.println("Inner field: " + innerField); // 可以访问局部内部类的成员变量
            }
        }

        InnerClass inner = new InnerClass(); // 在方法内部定义之后创建局部内部类的实例
        inner.innerMethod(); // 可以通过局部内部类的实例访问非静态成员
    }

    public static void main(String[] args) {
        OuterClass outer = new OuterClass();
        outer.outerMethod();
    }
}

在上面的代码中,我们在outerMethod()方法内部定义了一个局部内部类InnerClass,并在InnerClass中访问了外部类的成员变量outerField和final类型的局部变量finalVariable。注意,finalVariable在定义时被声明为final类型,因此可以在局部内部类中访问。在outerMethod()方法内部定义InnerClass之后,我们创建了它的实例,并通过该实例访问了它的成员方法innerMethod()。

6.那么对应的,外部类可以访问哪些内部类的成员呢?有限制吗?

外部类可以访问局部内部类的所有成员,包括其成员变量和方法。

但是,访问局部内部类的成员需要满足以下条件:

  • 局部内部类的实例必须先被创建。(我们刚刚提过了)
  • 局部内部类必须声明为 final。

第一个条件是因为局部内部类的实例只能在其所在的方法内部创建,所以外部类只能在创建该实例之后才能访问它的成员。

第二个条件是因为在访问局部内部类的成员时,必须使用该类的实例来访问。如果该类没有被声明为 final,那么在访问其成员时,编译器可能会出现误差。

我们具体说说第二个条件
第二个条件是指局部内部类必须被声明为 final 或者 effectively final。

在Java 8之前,访问局部内部类的成员变量和方法时,这些变量和方法所在的局部方法必须被声明为 final。这是因为Java的垃圾回收机制可能会在方法执行完毕后销毁这些变量和方法,如果局部内部类的实例还在使用这些变量和方法,就会引发错误。

在Java 8中,引入了 effectively final 的概念。如果一个局部变量在它的初始化后没有被重新赋值,那么它就可以被视为 effectively final。这样,即使不将局部内部类声明为 final,也可以在其中访问该局部变量。

需要注意的是,将局部内部类声明为 final 或者使用 effectively final 只是为了保证在方法执行完毕后,局部内部类的实例能够正常访问其成员变量和方法。如果局部内部类不需要在方法执行完毕后继续存在,那么这个条件可以不用满足。

7.编译器也有自己独立的字节码文件,命名格式如下:外部类名字$ 数字内部类名字.class

匿名内部类

匿名内部类是一种没有名称的局部内部类,通常用于定义一次性的、简单的类。它通常会实现一个接口或继承一个抽象类,并重写其中的方法。

下面是关于匿名内部类的语法规则和注意事项:

1.匿名内部类没有类名,因此只能创建它的一个对象,且该对象不能被复用。

2.匿名内部类通常用于创建实现某个接口或继承某个抽象类的对象。在创建匿名内部类时,必须实现该接口或抽象类的所有抽象方法。

3.同样的,匿名内部类可以访问外部类的成员变量和方法,但是访问局部变量时必须使用final修饰符,或在JDK 8及以上版本中,该变量必须是隐式的final变量。

4.匿名内部类的语法规则:new 接口名/父类名 ()   { //匿名内部类的实现代码 }。

下面是一个简单的示例代码,展示了如何定义和使用匿名内部类:

interface MyInterface {
    void printMessage();
}
public class AnonymousInnerClass {
    public void display() {
        new MyInterface() {
            @Override
            public void printMessage() {
               System.out.println("Hello, this is a message from anonymousinnerclass!");
            }
        }.printMessage();
    }

    public static void main(String[] args) {
        AnonymousInnerClass anonymous = new AnonymousInnerClass();
        anonymous.display();
    }

}
//或者
public class AnonymousInnerClass {
    public void display() {
        MyInterface obj = new MyInterface() {
            @Override
            public void printMessage() {
               System.out.println("Hello, this is a message from anonymousinnerclass!");
            }
        };
        obj.printMessage();
    }

    public static void main(String[] args) {
        AnonymousInnerClass anonymous = new AnonymousInnerClass();
        anonymous.display();
    }

    interface MyInterface {
        void printMessage();
    }
}

在上面的代码中,我们定义了一个匿名内部类,并实现了一个接口MyInterface的printMessage()方法。在display()方法中,我们创建了该匿名内部类的实例,并调用了printMessage()方法。由于匿名内部类没有类名,因此我们不能使用其它方式创建该类的实例,且该实例只能使用一次。

需要注意的是,在匿名内部类中访问外部类的成员变量和方法时,由于匿名内部类没有名字,因此无法使用this关键字访问外部类的成员。

在这种情况下,我们可以通过OuterClass.this.variableName的方式来访问外部类的成员变量,或OuterClass.this.methodName()的方式来访问外部类的方法。也就是直接使用外部类本身来调用其成员。

还有一点需要注意:匿名内部类不能访问被修改过的数据。

interface A {
    void test();
}
public class Test {
    public static void main(String[] args) {
        int val = 10;
        //val = 100;
        //如果上面那一行没有被注释掉,那么匿名内部类是无法访问它的

        A a = new A(){
            @Override
            public void test() {
                //默认在这里能访问的是被final修饰的
                System.out.println("值:"+ val);//在匿名内部类当中能够访问的是没有被修改过的数据-->变量的捕获
            }
        };
        a.test();


        System.out.println(val);
    }
}

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值