Java动态数组(二)
- 泛型
-
- [1. 什么是泛型](#1. 什么是泛型)
- [2. 定义泛型类](#2. 定义泛型类)
- [3. 用 E 代替类型](#3. 用 E 代替类型)
- [4. 不能用 E 来 new](#4. 不能用 E 来 new)
- [5. 使用时指定类型](#5. 使用时指定类型)
-
- [1. 数组引用](#1. 数组引用)
- [2. 单个数据](#2. 单个数据)
- [3. 构造方法参数](#3. 构造方法参数)
- [4. 方法参数](#4. 方法参数)
- [5. 返回值](#5. 返回值)
- [6. 局部变量](#6. 局部变量)
- 补充:三个关键字
- Object
泛型
1. 什么是泛型
泛型,说白了就是一种「类型模板」。
写动态数组 ZArrayList 的时候,我们暂时还不知道将来要装什么类型的数据------可能是 Integer,可能是 String,也可能是自定义的 Student。于是先用一个占位字母(通常叫 E,即 Element 元素)顶替这个「还不知道的类型」。
2. 定义泛型类
java
public class ZArrayList<E> {
// 类体
}
来个最简单的对比,普通类和泛型类都实现同一个效果:
java
// 普通类:类型固定为 String
public class NormalList {
public void add(String s) { ... }
}
java
// 泛型类:类型先用 E 占位,使用时再指定具体类型
public class ZArrayList<E> {
public void add(E e) { ... }
}
可以看到,同样是 add 方法:普通类把参数类型固定成了 String,所以它只能添加 String 类型的数据;而泛型类里写的是 E,这个 E 暂时还没有确定类型,所以将来使用时,既可以装 String,也可以装 Integer、Student 等其他类型。
区别就是:普通类把类型固定,泛型类先用 E 占位,等使用的时候再确定具体类型。
E叫「类型参数」,就是一个占位字母,你可以理解成"占位符";- 字母不一定是
E,写T、K、V都行,只是习惯上:E→ Element(元素)T→ Type(类型)K/V→ Key / Value(键和值)
3. 用 E 代替类型
java
public class ZArrayList<E> {
private E[] elements; // 1. 成员变量:数组引用类型
private E first; // 2. 成员变量:单个数据
public ZArrayList(E first) { // 3. 构造方法参数类型
this.first = first;
}
public void add(E e) { ... } // 4. 普通方法参数类型
public E get(int index) { ... } // 5. 方法返回值类型
public void show() {
E temp = first; // 6. 局部变量类型
}
}
在类里面,凡是需要写"数据类型"的位置,都可以直接用 E 占位。
但存在两个容易犯错的地方:
E[]只能用来声明 数组引用,不能写成new E[10],下一点细讲。- 静态方法(
static)里不能直接用类的E:因为静态方法属于类本身,还没和具体类型绑定。
4. 不能用 E 来 new
java
E e = new E(); // ❌ 编译错误
E[] arr = new E[10]; // ❌ 编译错误
原因:编译阶段 Java 还不知道 E 到底是什么类型,没法给它分配确定的内存,所以不能用 E 去 new。这也是泛型最容易犯的错误。
5. 使用时指定类型
先看使用时的写法:
java
// 创建对象时,把 E 换成具体类型
ZArrayList<String> list = new ZArrayList<>();
创建对象时,E 指定成什么类型,类里所有 E 就一起变成那个类型。指定成 String,这个数组就专门用来存 String;指定成 Integer,就专门用来存 Integer。
下面把第 3 节的 6 种 E 一个一个拆开讲,看看它们到使用阶段分别变成什么、有什么用。
1. 数组引用
原来的写法:
java
private E[] elements;
指定 String 后变成:
java
private String[] elements;
作用:这是数组本体,用来把多个数据存进对象里。
看到 private E[] elements; 可能会疑惑:这里并没有像方法参数 add(String s) 那样,在括号里看到有数据传进来啊。
其实,成员变量不是靠"传参"那一瞬间存数据的,它是对象内部的"仓库"。声明 private E[] elements; 只是先准备好一个数组引用,相当于先把货架搭好。
所以第 1 种 E 的意思就是:给这个仓库定一个"专门放什么"。
比如你写 ZArrayList<String>,就相当于给这个仓库贴了个标签:这里只能放 String 。于是 elements 这个数组就变成了"字符串数组"
2. 单个数据
原来的写法:
java
private E first;
指定 String 后变成:
java
private String first;
作用:单独保存一个数据,比如想记住第一个元素。
理解:它和数组一样都是"仓库",只不过数组是存一批,这个是存一个。
3. 构造方法参数
原来的写法:
java
public ZArrayList(E first) {
this.first = first;
}
指定 String 后变成:
java
public ZArrayList(String first) {
this.first = first;
}
作用:创建对象时,规定必须传入一个 String。
理解:new ZArrayList<>() 的括号里只能放 String:
java
// 正确:
ZArrayList<String> list = new ZArrayList<>("hello");
// 错误,不能传 int:
// ZArrayList<String> list = new ZArrayList<>(100);
4. 方法参数
原来的写法:
java
public void add(E e) { ... }
指定 String 后变成:
java
public void add(String e) { ... }
作用:调用 add 时,只能往里面放 String。
java
list.add("apple"); // 正确
// list.add(123); // 编译报错
5. 返回值
原来的写法:
java
public E get(int index) { ... }
指定 String 后变成:
java
public String get(int index) { ... }
作用:从数组里取出数据时,直接就是 String。
理解:不用再自己强转:
java
String s = list.get(0); // 直接是 String
// 如果不用泛型,可能还得自己转一次:
// String s = (String) list.get(0);
6. 局部变量
原来的写法:
java
public void show() {
E temp = first;
}
指定 String 后变成:
java
public void show() {
String temp = first;
}
作用:方法内部临时保存数据时使用。
理解:临时变量也要和 E 保持一致,否则类型对不上会报错。
小结 :这 6 种 E 归根到底就是三件事------存数据 (第 1、2 种)、收数据 (第 3、4 种)、给数据 (第 5、6 种)。使用时指定 String,这三件事就都围绕 String 来运转。
补充:三个关键字
我们来简单区分这三个关键字:
private:私有,只能在这个类内部 使用。比如private E[] elements就是不想让外部随便乱动数据,从而更安全。public:公共,类外面 也能访问。比如public void add(E e),别人创建对象后,才能调用这个方法添加数据。static:静态,属于类本身 ,不依赖某个具体的对象开放。因为没有和具体对象绑定,所以静态方法里不能用类的E。
再补充一下 static:
static 表示"属于类本身,不属于某个具体对象"。类一加载,静态成员就已经存在了,不需要先 new 对象就能直接使用。
Java 的程序入口也和它有关。每个 Java 程序都要从一个特殊的 main 方法开始运行,最常用的主类写法是:
java
public class Main {
public static void main(String[] args) {
// 所有代码从这里开始执行
}
}
为什么 main 方法前面一定要写 static?因为程序刚启动时,JVM 还没有创建任何一个对象,它只能通过"类名.方法名"的方式直接调用 main。如果 main 不是 static,JVM 就找不到程序入口,程序根本没法运行。
竞赛中写主类,记住这个固定套路就行:
- 类名一般写成
Main:很多 OJ(在线判题系统)会强制要求public class Main。 main方法必须完整写成public static void main(String[] args),其中static不能漏。
所以可以这样理解:main 是一个静态方法,它属于 Main 类本身;程序一启动,还没创建任何对象,JVM 就能直接调用 Main.main() 开始运行。
Object
理解定义
Object 是 Java 里最基础的一个类,你可以把它理解成所有类的"老祖宗"。
在 Java 里,不管你写一个什么样的类,哪怕什么都没有,它都会自动继承 Object。
比如你写一个最简单的类:
java
public class Student {
// 里面什么都没写
}
其实它背后等价于:
java
public class Student extends Object {
// Java 自动帮你继承 Object
}
也就是说,Student、String、Integer,还有我们前面写的 ZArrayList,它们共同的祖先都是 Object。
重写方法
前面说过,Object 是所有类的"老祖宗",所以它会准备好一些通用方法,让每个类都能直接用。这些方法虽然能用,但往往只是"通用版",并不一定符合我们自己的需求。
比如 Object 给我们准备了一个 toString() 方法,作用是"把对象翻译成字符串"。但这个通用版没有太多信息,默认只会告诉我们:这是哪个类、以及一个看不懂的编号。
好在 Java 允许我们"重写":子类可以重新写一个名字相同、参数相同、返回值相同的方法,把从 Object 继承来的通用版本盖掉,换成更适合自己的版本。
重点记住一句话:继承来的方法,如果不满意,就可以重写它。
下面就用 toString() 来看具体怎么做。
默认效果
先写一个最普通的 Student,里面只放两个信息:
java
public class Student {
String name;
int age;
}
这时候我们没有重写任何方法,打印它:
java
Student stu = new Student();
System.out.println(stu);
会看到:
text
Student@1b6d3586
这其实是 Object 默认的 toString() 输出:类名@地址编号(哈希值)。它知道"这是一个 Student 对象",但不知道我们真正关心的是 name 和 age。
重写 toString()
现在要求变清楚:我们希望打印学生时,能直接看到姓名和年龄。于是我们在 Student 里重写 toString():
java
public class Student {
String name;
int age;
@Override
public String toString() {
return "Student{name=" + name + ", age=" + age + "}";
}
}
解释两点:
@Override:这是 Java 的标记,意思是"我是在重写从父类继承来的方法,不是新写一个方法"。- 方法名、参数、返回值都和
Object里的toString()保持一致,才能真正覆盖它。
重写后,再打印同一个对象:
text
Student{name=小明, age=18}
现在输出就是我们想要的信息了。
前后对比
- 不重写:用
Object给的通用版,只能看到Student@1b6d3586。 - 重写后:用
Student自己定制的版本,能直接看到name和age。
以后做动态数组时也会用同样的思路:给 ZArrayList 重写 toString(),让打印整个数组时显示的是里面的元素,而不是一串看不懂的编号。