内部类

内部类介绍

内部类分四种:

  • 成员内部类

  • 静态内部类

  • 局部内部类

  • 匿名内部类

示意图:


图 1
类的五大成员: 属性, 方法, 构造方法, 代码块, 内部类.

内部类: 在一个类的里面, 再定义一个类.

举例: 在 A 类的内部定义 B 类, B 类就被称为内部类.

public class Outer { // 外部类
public class Inner { // 内部类
}
}
既不是外部类也不是内部类的类统称为外部其他类.

可以在外部其他类中创建内部类的对象, 并调用它的方法.

内部类表示的事物是外部类的一部分.

内部类单独出现没有任何意义.

例如:
需求: 写一个 Javabean 类描述汽车.
属性: 汽车的品牌, 车龄, 颜色, 发动机的品牌, 使用年限.

可以定义一个车类:


图 2

但不是太好, 因为发动机是一个独立的个体, 和车还是有点区别的, 所以和发动机相关的属性, 不应该和车定义在一起. 于是可以将发动机的属性拿出来, 再定义一个发动机类:


图 3

但还不是太好, 因为发动机如果单独出现, 是没有什么实际意义的. 所以最好的解决方案就是把发动机这个类定义在车这个类的里面:


图 4
内部类的访问特点:
  • 内部类可以直接访问外部类的成员, 包括私有.

  • 外部类要访问内部类的成员, 必须创建对象, 外部类不能直接访问内部类的成员.

什么时候用到内部类: 如果 B 类表示的事物是 A 类的一部分, 且 B 类单独存在没有意义, 那么就可以将 B 类设计为 A 类的内部类. 例如汽车的发动机, 人的心脏, ArrayList 的迭代器.

程序示例:

public class Car {
String carName;
private int age;
String carColor;
public void show() {
System.out.println(carColor); // 外部类的成员方法中, 获取外部类的成员变量, 没有任何问题
// System.out.println(engineAge); // 报错, 外部类要访问内部类的成员, 必须创建对象
Engine e = new Engine(); // 创建内部类的对象
System.out.println(e.engineName); // 使用内部类的对象访问内部类的成员变量
}
class Engine {
String engineName;
int engineAge;
public void show() {
System.out.println(engineName); // 内部类中访问内部类的成员变量, 完全没问题
System.out.println(carName); // 内部类可以直接访问外部类的成员, 包括私有
System.out.println(age); // 内部类可以直接访问外部类的成员, 包括私有
}
}
}

图 5 外部类要访问内部类的成员, 必须创建对象

查看 ArrayList 类中的内部类:


图 6

集合的作用就是存储数据的, 可以把数据放进集合, 也可以通过遍历的方式把数据从集合中获取出来. 但是 ArrayList 有很多种, 之前用过最简单的 for 循环. 但是除了最简单的 for 循环之外, 还有很多种遍历方式. 迭代器则是一种比较高级的遍历方式. 这种遍历方式对于集合而言, 是相对独立的, 但是又属于集合, 所以 Java 就将迭代器这个类设计成了 ArrayList 这个类的内部类.


图 7 ArrayList 类中的 Itr 内部类

成员内部类

写在成员位置的 (类中, 方法外), 属于外部类的成员. 例如:


图 1

成员内部类的地位和成员变量和成员方法的地位是一模一样的.

成员内部类的三个主要问题:


图 2

成员内部类可以被一些修饰符所修饰, 比如: private, 默认, protected, public, static 等.

只要是可以修饰成员变量的, 都可以修饰成员内部类, 且规则也相同.

如果用 private 修饰内部类, 那么在外部类的外面, 就不能创建内部类的对象, 只能在外部类的里面, 内部类的外面创建内部类的对象.

如果不写, 即默认权限, 就只能在本包当中用.

用 static 修饰的内部类, 称为静态内部类.

一般情况下, 凡事说到成员内部类, 一定不是用 static 修饰的, 如果是用 static 修饰的, 就不叫成员内部类, 叫静态内部类.

在成员内部类里面, JDK 16 之前不能定义静态变量, JDK 16 开始才可以定义静态变量.

获取成员内部类的两种方式:

  • 方式一: 在外部类中编写方法, 对外提供内部类的对象.

  • 方式二: 在外界直接创建内部类的对象, 格式: 外部类名.内部类名 对象名 = 外部类对象.内部类对象

一般而言, 可以用链式编程的方式创建一个类的对象并访问其成员变量. 程序示例:

Javabean 类:

public class Outer {
String name;
public class Inner {
}
}

测试类:

public class Test {
public static void main(String[] args) {
// 使用链式编程创建 Outer 类的对象, 并使用这个对象来访问其成员变量
System.out.println(new Outer().name); // 输出 null
}
}

如果内部类是 public 的, 则可以在外部其他类中直接创建这个内部类的对象.

Javabean 类:

public class Outer {
String name;
public class Inner {
}
}

测试类:

public class Test {
public static void main(String[] args) {
// 内部类 Inner 是 public 的, 可以在外部其他类中直接创建这个内部类的对象
// 这个内部类 Inner 的对象的类型是 Outer.Inner
Outer.Inner oi = new Outer().new Inner();
}
}

当内部类是私有时, 在外部其他类中, 不能直接创建该内部类的对象, 可以在外部类中定义一个 public 的方法对外提供这个内部类的对象. 程序示例:

Javabean 类:

public class Outer {
String name;
private class Inner {
}
public Inner getInstance() {
return new Inner();
}
}

测试类:

public class Test {
public static void main(String[] args) {
// Inner 在 Outer 类中是私有的, 外界并不知道 Inner 这个类的存在
// Outer.Inner oi = new Outer().new Inner(); // 报错: 'innerclass.Outer.Inner' has private access in 'innerclass.Outer'
// 创建 Outer 类的对象, 用这个对象调用 Outer 类中提供的对外获取私有内部类的方法
Outer outer = new Outer();
outer.getInstance(); // 获取到了内部类的对象, 但是没有使用这个对象
}
}

通过这种方式获得的内部类的对象, 其类型如何定义?

错误示范:

public class Test {
public static void main(String[] args) {
// Inner 在 Outer 类中是私有的, 外界并不知道 Inner 这个类的存在
// Outer.Inner oi = new Outer().new Inner(); // 报错: 'innerclass.Outer.Inner' has private access in 'innerclass.Outer'
// 创建 Outer 类的对象, 用这个对象调用 Outer 类中提供的对外获取私有内部类的方法
Outer outer = new Outer();
Outer.Inner oi = outer.getInstance(); // 报错: 'innerclass.Outer.Inner' has private access in 'innerclass.Outer'
}
}

方法: 将内部类的对象的类型定义为该内部类的父类类型, 形成多态. 此处内部类 Inner 没有指定的父类, 则继承自 Object. 上述代码改写为:

public class Test {
public static void main(String[] args) {
// Inner 在 Outer 类中是私有的, 外界并不知道 Inner 这个类的存在
// Outer.Inner oi = new Outer().new Inner(); // 报错: 'innerclass.Outer.Inner' has private access in 'innerclass.Outer'
// 创建 Outer 类的对象, 用这个对象调用 Outer 类中提供的对外获取私有内部类的方法
Outer outer = new Outer();
Object oi = outer.getInstance(); // 不再报错
}
}

另一种解决方式为不要定义这个对象, 而是直接使用这个对象. 上述代码改写为:

public class Test {
public static void main(String[] args) {
// Inner 在 Outer 类中是私有的, 外界并不知道 Inner 这个类的存在
// Outer.Inner oi = new Outer().new Inner(); // 报错: 'innerclass.Outer.Inner' has private access in 'innerclass.Outer'
// 创建 Outer 类的对象, 用这个对象调用 Outer 类中提供的对外获取私有内部类的方法
Outer outer = new Outer();
System.out.println(outer.getInstance());
}
}

执行结果:

innerclass.Outer$Inner@4eec7777

从这个执行结果可以看出, Java 在表示内部类的类型的时候, 是通过 , 的左边是外部类的类名, $ 的右边是内部类的类名.

程序示例:

Javabean 类:

public class Outer { // 外部类
class Inner1{ // 内部类 1
}
private class Inner2{ // 内部类 2
}
public Inner2 getInstance(){ // 在外部类中编写方法, 对外提供内部类的对象
return new Inner2();
}
}

测试类:

public class Test {
public static void main(String[] args) {
Outer.Inner1 oi1 = new Outer().new Inner1(); // 创建内部类对象, 链式编程
// Outer.Inner2 oi2 = new Outer().new Inner2(); // 报错: 'demo1.Outer.Inner2' has private access in 'demo1.Outer'
Outer o = new Outer();
// Outer.Inner2 oi2 = o.getInstance(); // 报错, Inner2 是私有类型, 外界根本不知道这个类型
// 两种解决方式:
// 1. 用 Inner2 的父类类型, 形成多态
// 此处 Inner2 没有父类, 就默认继承自 Object
Object io2 = o.getInstance();
// 第 2 种解决方法: 不要给这个对象起名字, 直接用这个对象:
System.out.println(o.getInstance()); // demo1.Outer$Inner2@3b07d329
// java 里面, 表示内部类的时候, 是用 $ 进行区分的, $ 的左边是外部类的类名, $ 的右边是内部类的类名
}
}

内部类中定义了一个静态成员变量:

public class Outer { // 外部类
class Inner1 { // 内部类 1
static String name; // 内部类中定义了一个静态成员变量
}
private class Inner2 { // 内部类 2
}
public Inner2 getInstance() { // 在外部类中编写方法, 对外提供内部类的对象
return new Inner2();
}
}

当前 JDK 版本为 17, 因此不会报错.

切换当前项目到低版本的 JDK:


图 3

现在切换到低于 JDK 16 的版本, 报错了:


图 4

访问重名变量的方法:

程序示例:

Javabean 类:

public class Outer {
String name = "zhangsanInOuter";
public class Inner {
String name = "zhangsanInInner";
public void show() {
String name = "zhangsanInShow";
System.out.println(name); // zhangsanInShow
System.out.println(this.name); // zhangsanInInner
System.out.println(Outer.this.name); // zhangsanInOuter
}
}
}

测试类:

public class Test {
public static void main(String[] args) {
Outer.Inner oi = new Outer().new Inner();
oi.show();
}
}

程序示例:

Javabean 类:

public class Outer3 {
private int a = 10;
class Inner3 {
private int a = 20;
public void show() {
int a = 30;
System.out.println(a); // 30
System.out.println(this.a); // 20
System.out.println(Outer3.this.a); // 10
}
}
}

测试类:

public class Test {
public static void main(String[] args) {
Outer3.Inner3 oi = new Outer3().new Inner3();
oi.show();
}
}

内部类和外部类在编译后, 是两个互相独立的字节码文件:


图 5

图 6 内部类的内存图

内存分析:

第一步, Test 类的字节码文件 Test.class 加载到方法区, 然后虚拟机会自动调用 main() 方法, main() 方法加载进栈. 开始执行 main() 方法中的第一行代码.

第二步, 执行代码 Outer3.Inner3 oi = new Outer3().new Inner3();, 发现用到了 Outer3 类和 Inner3 类, 所以会把外部类和内部类的字节码文件加载到方法区中. 外部类和内部类在内存中是两个独立的字节码文件. 把两个字节码文件加载到方法区后, 才真正地开始执行这一行代码.

等号的左边, 即 Outer3.Inner3 oi, 在栈中创建了一个变量 oi, 类型为 Outer3.Inner3, 就表示 oi 可以记录 Outer3 类型的对象里面的 Inner3 这个类型的对象的地址值.

接下来是等号的右边, 即 new Outer3().new Inner3();, 可以分为两个部分, 第一部分是 new Outer3(), 这是在堆空间中创建了一个外部类的对象, 这里面记录了成员变量 a 的值, 为 10. 假设这部分小空间的地址值为 001. 第一部分执行完毕, 再来看第二部分, 即 new Inner3(), 又是一个 new 关键字, 于是再次在堆内存中开辟一块空间, 假设这一小块空间的地址值为 002. 这块空间用来存放内部类的对象, 存储了成员变量的信息, 记录的 a 的值为 20. 同时, JVM 会给内部类的对象加一个隐藏的成员变量 this, 用来记录在第一部分中创建的外部类的对象的地址值. 到此, 等号右边的代码也执行完毕了. 于是要把内部类对象的那部分空间的地址值, 即 002, 赋值给变量 oi.

第三步, 执行 oi.show();, show() 方法进栈, 方法的调用者为 oi, 于是 this 记录的就是调用者 oi 的地址值 002. 进入 show() 方法的方法体. 方法 show() 里面记录了局部变量 a 的值 30. 如果执行 System.out.println(a); 直接输出 a, 会触发就近原则, 先到本方法里面找, 直接就找到了, 就打印本方法中的 a 即 30. 如果执行 System.out.println(this.a);, this 记录的是调用者 oi 的地址值 002, 于是打印的是 002 里面的 a, 于是打印 20. 如果执行 System.out.println(Outer3.this.a);, 最前面有 Outer3.this, 于是先到 002 空间中找到 this 的地址 001, 然后去找 001 里面的 a 的值. Outer3.this 就表示获取了外部类的对象的地址值, 简单说就是 Outer3.this 就表示外部类的对象.

静态内部类

静态内部类是成员内部类的一种, 是一种特殊的成员内部类.


图 1
静态内部类只能访问外部类中的静态变量和静态方法, 如果想要访问外部类中非静态的成员, 则需要创建外部类的对象.

程序示例:

public class Car {
public String name;
public int year;
public static void testMethod() {
// 静态方法访问非静态的成员, 报错
// System.out.println("Car name " + name); // Non-static field 'name' cannot be referenced from a static context
// 创建 Car 的对象, 用这个对象来访问非静态的成员
Car car = new Car();
System.out.println("Car name: " + car.name);
}
}

创建静态内部类的对象的格式:

外部类名.内部类名 对象名 = new 外部类名.内部类名();

因为是静态的, 等号右侧不用先创建外部类的对象, 而是可以直接用外部类的类名. 像成员内部类那种方式, 是先写 new 外部类名(), 即先创建外部类对象.

调用非静态方法的格式: 先创建对象, 用对象调用.

调用静态方法的格式:

外部类名.内部类名.方法名();

程序示例:

Javabean 类:

public class Outer {
int a = 10; // 非静态的成员变量
static int b = 20; // 静态的成员变量
// 静态内部类
static class Inner {
public void show1() {
// 静态内部类只能访问外部类中的静态成员变量和静态方法
// 如果想要访问外部类中非静态的成员, 则需要创建外部类的对象
Outer o = new Outer(); // 创建外部类的对象
System.out.println(o.a); // 用外部类的对象访问非静态的成员变量
System.out.println(b); // 直接访问外部类的静态成员变量
System.out.println("静态内部类中, 非静态的方法 public void show1() 被调用了");
}
public static void show2() {
Outer o = new Outer();
System.out.println(o.a);
System.out.println(b);
System.out.println("静态内部类中, 静态方法 public static void show2() 被调用了");
}
}
}

测试类:

public class Test {
public static void main(String[] args) {
// 创建静态内部类的对象
// 只要是静态的东西, 都可以直接用类名来获取
Outer.Inner oi = new Outer.Inner();
// 用对象调用非静态的方法
oi.show1();
// 用对象调用静态方法, 也是可以的, 但是不提倡, 静态的东西 (包括静态成员变量和静态成员方法) 用对象来访问当然行得通, 但是不提倡, 可以参考名为 static 的博客
// IDEA 不会进行代码提示, 但是强行直接书写, 也不会报错
oi.show2();
// 提倡的调用静态方法的方式
Outer.Inner.show2();
}
}

局部内部类

局部内部类: 将内部类定义在方法里面, 就叫做局部内部类. 类似方法里面的局部变量. 能用来修饰局部变量的, 都能用来修饰局部内部类. 例如可以用 final 来修饰局部变量, 所以可用 final 修饰局部内部类. public 等访问权限修饰符只能用来修饰成员, 不能用来修饰局部变量, 所以也不能用来修饰局部内部类.

外界无法直接使用局部内部类, 需要在定义了局部内部类的这个方法里面创建局部内部类的对象才能使用.

局部内部类可以直接访问外部类的成员, 也可以访问方法内的局部变量.

能用来修饰局部变量的都能用来修饰局部内部类, 不能用来修饰局部变量的都不能用来修饰局部内部类. 例如可以用 final 来修饰局部变量, 此时局部变量就变成了常量, 同理也可以用 final 来修饰局部内部类.

程序示例:

public class Outer2 {
int a = 10;
static int b = 20;
public static void show1() {
int c = 30;
// static int d=40; // Modifier 'static' not allowed here
class Inner {
int e = 50;
static int f = 60;
public void method1() {
// System.out.println(a); // Non-static field 'a' cannot be referenced from a static context
System.out.println(b);
System.out.println(c);
System.out.println(e);
System.out.println(f);
}
public static void method2() {
// System.out.println(a); // Non-static field 'a' cannot be referenced from a static context
System.out.println(b);
// System.out.println(c); // Non-static variable 'c' cannot be referenced from a static context
// System.out.println(e); // Non-static field 'e' cannot be referenced from a static context
System.out.println(f);
}
}
// 在方法里面创建局部内部类的对象
Inner inner = new Inner();
inner.method1();
inner.method2();
}
public void show2() {
int c = 30;
// static int d=40; // Modifier 'static' not allowed here
class Inner {
int e = 50;
static int f = 60;
public void method1() {
System.out.println(a); // 这个 a 可以被访问
System.out.println(b);
System.out.println(c);
System.out.println(e);
System.out.println(f);
}
public static void method2() {
// System.out.println(a); // Non-static field 'a' cannot be referenced from a static context
System.out.println(b);
// System.out.println(c); // Non-static variable 'c' cannot be referenced from a static context
// System.out.println(e); // Non-static field 'e' cannot be referenced from a static context
System.out.println(f);
}
}
// 在方法里面创建局部内部类的对象
Inner inner = new Inner();
inner.method1();
inner.method2();
}
}

测试类:

public class Test2 {
public static void main(String[] args) {
Outer2.show1();
System.out.println("------------------");
Outer2 o = new Outer2();
o.show2();
}
}

执行结果:

20
30
50
60
20
60
------------------
10
20
30
50
60
20
60

程序示例:

public class Outer {
int b = 20;
public void show() {
int a = 10;
// 局部内部类
class Inner {
String name;
int age;
int c = 30;
public void show1() {
System.out.println("局部内部类中的 public void show1() 方法.");
}
public static void show2() {
System.out.println("局部内部类中的 public static void show2() 方法.");
}
public void show3() {
System.out.println(a); // 访问方法里面的局部变量 a
System.out.println(b); // 访问外部类的成员变量 b
System.out.println(c);
}
}
// 创建局部内部类的对象
Inner ii = new Inner();
System.out.println(ii.age);
System.out.println(ii.name);
ii.show1();
Inner.show2();
ii.show3();
}
}

测试类:

public class Test {
public static void main(String[] args) {
Outer o = new Outer();
o.show();
}
}

执行结果:

0
null
局部内部类中的 public void show1() 方法.
局部内部类中的 public static void show2() 方法.
10
20
30

匿名内部类

匿名内部类的本质就是隐藏了名字的内部类.

格式:

new 类名或者接口名() {
重写方法(接口里面的所有的抽象方法);
};

图 1

 


图 2

图 3

图 4

图 4 左边的写法中, 从 Swim 右边的那个花括号开始, 到最后一个花括号结束, 是类的真正的内容.


图 5

可以用这个没有名字的类来实现 Swim 接口. 方法是将 Swim 写在这个没有名字的类的前面, 然后在类中重写接口的所有抽象方法:


图 6

还可以用这个类来创建对象.


图 7

把 new 放到 接口名 Swim 前面, 这是一个没有类名的类, 所以传统方法中的类名就不要了, 再将传统方法中的 () 放到接口名 Swim 的后面, 再将传统方法中的 ; 放到这个没有名字的类的最后面.


图 8

图 9 匿名内部类实现了一个接口

图 9 包含了三种关系:

  1. 继承/实现, 指的是没有名字的类和其前面的父类或者接口之间的关系, 此处 Swim 为接口, 所以此处是实现关系.

  2. 方法的重写, 此处这个没有名字的类, 实现了接口 Swim, 所以类中重写了接口的抽象方法.

  3. 创建对象, 指的是用 new 关键字创建了这个没有名字的类的对象.

因此, 从语法角度来说, 图 9 并不是匿名内部类, 而是匿名内部类的对象, 真正的匿名内部类是那个没有名字的类. 这个没有名字的类实现了接口 Swim, 所以是接口 Swim 的实现类, 所以整体就是 Swim 的实现类的对象.

当匿名内部类继承另一个类时:


图 10 匿名内部类继承另一个类

同理, 从语法角度来说, 图 10 并不是匿名内部类, 而是匿名内部类的对象, 真正的匿名内部类是那个没有名字的类. 这个匿名内部类继承了 Animal 类, 所以是 Animal 类的子类, 所以整体就是 Animal 这个类的子类的对象.

程序示例:

一个接口:

public interface Swim {
public abstract void swim();
}

一个抽象类:

public abstract class Animal {
public abstract void eat();
}

测试类:

public class Test {
public static void main(String[] args) {
// 匿名内部类
new Swim() {
@Override
public void swim() {
System.out.println("重写了游泳的方法.");
}
};
new Animal() {
@Override
public void eat() {
System.out.println("重写了 eat 方法.");
}
};
}
}

这个测试类运行时, 将不会产生任何输出, 因为只是创建了匿名内部类的对象, 这个对象并没有被使用.

匿名内部类也会产生字节码文件:


图 11

可以看见, 匿名内部类其实是有名字的, 只是不需要我们自己去写而已.

反编译: 把字节码文件变回我们能看得懂的 Java 代码.


图 12

图 13

反编译的结果中, demo5$Test1demo5$Test2 中的 Test1 和 Test2 就是没有名字的类的名字, 是 JVM 帮我们命名的.

程序示例:

public abstract class Animal {
public abstract void eat();
}
public class Dog extends Animal{
@Override
public void eat() {
System.out.println("狗吃骨头");
}
}
public interface Swim {
public abstract void swim();
}

测试类 1:

public class Test {
public static void main(String[] args) {
// 编写匿名内部类的代码
new Swim() {
@Override
public void swim() {
System.out.println("重写了游泳的方法");
}
};
new Animal() {
@Override
public void eat() {
System.out.println("重写了eat方法");
}
};
// 在测试类中调用下面的 method 方法? method 方法传递的形参是 Animal 类型, 而 Animal 是抽象类, 不能创建 Animal 类型的对象
// 以前的方式如何调用?
// 要自己写一个子类 (非抽象类) 继承 Animal 类
// 再创建子类的对象, 传递给 method 方法, 写法为:
// Dog d = new Dog();
// method(d);
// 如果 Dog 类我只要用一次, 那么还需要单独定义一个类太麻烦了.
method(
new Animal() {
@Override
public void eat() {
System.out.println("狗吃骨头");
}
} // 这里将这个匿名内部类的对象作为实参传递给 method 方法的形参 a, 不是一条语句, 所以最后不要加分号.
);
}
public static void method(Animal a) { // Animal a = 子类对象; 用多态的形式
a.eat(); // 编译看左边, 运行看右边, 编译时看 Animal 这个类有没有 eat() 方法, 执行时真正执行的是子类中的重写的那个 eat() 方法, 即打印狗吃骨头
}
}

测试类 2:

public class Test2 {
// 放在成员的位置了, 是一个没有名字的成员内部类. 如果放在方法里面, 就是一个没有名字的局部内部类.
Swim s = new Swim() {
@Override
public void swim() {
System.out.println("重写之后游泳方法");
}
};
public static void main(String[] args) {
// 整体我们可以理解为 Swim 接口的实现类对象, 可以将这个对象赋值给接口 Swim 类型的变量, 接口也是一个类型, 就像类一样.
// 接口多态
Swim s = new Swim() {
@Override
public void swim() {
System.out.println("重写之后游泳方法");
}
};
// 接口多态同样遵循编译看左边, 运行看右边的原则
s.swim(); // 输出: 重写之后游泳方法
new Swim() {
@Override
public void swim() {
System.out.println("重写之后游泳方法");
}
}.swim(); // 既然这是一个对象, 就可以直接调用自己这个类里面所有的方法. 除了 swim() 方法, 还可以调用一些从 Object 继承下来的方法
}
}

图 14

匿名内部类的使用场景: 当方法的参数是接口或者类时, 以接口为例, 可以传递这个接口的实现类的对象, 如果实现类只需要使用一次, 就可以使用匿名内部类简化代码.

posted @   有空  阅读(13)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
点击右上角即可分享
微信分享提示