Java动态数组(二)
- 泛型
- 1. 什么是泛型
- 2. 定义泛型类
- 3. 用 E 代替类型
- 4. 不能用 E 来 new
- 5. 使用时指定类型
- 1. 数组引用
- 2. 单个数据
- 3. 构造方法参数
- 4. 方法参数
- 5. 返回值
- 6. 局部变量
- 补充:三个关键字
- Object
- 理解定义
- 重写方法
- 默认效果
- 重写 toString()
- 前后对比
泛型
1. 什么是泛型
泛型,说白了就是一种「类型模板」。
写动态数组ZArrayList的时候,我们暂时还不知道将来要装什么类型的数据——可能是Integer,可能是String,也可能是自定义的Student。于是先用一个占位字母(通常叫E,即 Element 元素)顶替这个「还不知道的类型」。
2. 定义泛型类
publicclassZArrayList<E>{// 类体}来个最简单的对比,普通类和泛型类都实现同一个效果:
// 普通类:类型固定为 StringpublicclassNormalList{publicvoidadd(Strings){...}}// 泛型类:类型先用 E 占位,使用时再指定具体类型publicclassZArrayList<E>{publicvoidadd(Ee){...}}可以看到,同样是add方法:普通类把参数类型固定成了String,所以它只能添加String类型的数据;而泛型类里写的是E,这个E暂时还没有确定类型,所以将来使用时,既可以装String,也可以装Integer、Student等其他类型。
区别就是:普通类把类型固定,泛型类先用E占位,等使用的时候再确定具体类型。
E叫「类型参数」,就是一个占位字母,你可以理解成"占位符";- 字母不一定是
E,写T、K、V都行,只是习惯上:E→ Element(元素)T→ Type(类型)K/V→ Key / Value(键和值)
3. 用 E 代替类型
publicclassZArrayList<E>{privateE[]elements;// 1. 成员变量:数组引用类型privateEfirst;// 2. 成员变量:单个数据publicZArrayList(Efirst){// 3. 构造方法参数类型this.first=first;}publicvoidadd(Ee){...}// 4. 普通方法参数类型publicEget(intindex){...}// 5. 方法返回值类型publicvoidshow(){Etemp=first;// 6. 局部变量类型}}在类里面,凡是需要写“数据类型”的位置,都可以直接用E占位。
但存在两个容易犯错的地方:
E[]只能用来声明数组引用,不能写成new E[10],下一点细讲。- 静态方法(
static)里不能直接用类的E:因为静态方法属于类本身,还没和具体类型绑定。
4. 不能用 E 来 new
Ee=newE();// ❌ 编译错误E[]arr=newE[10];// ❌ 编译错误原因:编译阶段 Java 还不知道E到底是什么类型,没法给它分配确定的内存,所以不能用E去 new。这也是泛型最容易犯的错误。
5. 使用时指定类型
先看使用时的写法:
// 创建对象时,把 E 换成具体类型ZArrayList<String>list=newZArrayList<>();创建对象时,E指定成什么类型,类里所有E就一起变成那个类型。指定成String,这个数组就专门用来存String;指定成Integer,就专门用来存Integer。
下面把第 3 节的 6 种E一个一个拆开讲,看看它们到使用阶段分别变成什么、有什么用。
1. 数组引用
原来的写法:
privateE[]elements;指定String后变成:
privateString[]elements;作用:这是数组本体,用来把多个数据存进对象里。
看到private E[] elements;可能会疑惑:这里并没有像方法参数add(String s)那样,在括号里看到有数据传进来啊。
其实,成员变量不是靠“传参”那一瞬间存数据的,它是对象内部的“仓库”。声明private E[] elements;只是先准备好一个数组引用,相当于先把货架搭好。
所以第 1 种E的意思就是:给这个仓库定一个“专门放什么”。
比如你写ZArrayList<String>,就相当于给这个仓库贴了个标签:这里只能放String。于是elements这个数组就变成了“字符串数组”
2. 单个数据
原来的写法:
privateEfirst;指定String后变成:
privateStringfirst;作用:单独保存一个数据,比如想记住第一个元素。
理解:它和数组一样都是“仓库”,只不过数组是存一批,这个是存一个。
3. 构造方法参数
原来的写法:
publicZArrayList(Efirst){this.first=first;}指定String后变成:
publicZArrayList(Stringfirst){this.first=first;}作用:创建对象时,规定必须传入一个String。
理解:new ZArrayList<>()的括号里只能放String:
// 正确:ZArrayList<String>list=newZArrayList<>("hello");// 错误,不能传 int:// ZArrayList<String> list = new ZArrayList<>(100);4. 方法参数
原来的写法:
publicvoidadd(Ee){...}指定String后变成:
publicvoidadd(Stringe){...}作用:调用add时,只能往里面放String。
list.add("apple");// 正确// list.add(123); // 编译报错5. 返回值
原来的写法:
publicEget(intindex){...}指定String后变成:
publicStringget(intindex){...}作用:从数组里取出数据时,直接就是String。
理解:不用再自己强转:
Strings=list.get(0);// 直接是 String// 如果不用泛型,可能还得自己转一次:// String s = (String) list.get(0);6. 局部变量
原来的写法:
publicvoidshow(){Etemp=first;}指定String后变成:
publicvoidshow(){Stringtemp=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方法开始运行,最常用的主类写法是:
publicclassMain{publicstaticvoidmain(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。
比如你写一个最简单的类:
publicclassStudent{// 里面什么都没写}其实它背后等价于:
publicclassStudentextendsObject{// Java 自动帮你继承 Object}也就是说,Student、String、Integer,还有我们前面写的ZArrayList,它们共同的祖先都是Object。
重写方法
前面说过,Object是所有类的“老祖宗”,所以它会准备好一些通用方法,让每个类都能直接用。这些方法虽然能用,但往往只是“通用版”,并不一定符合我们自己的需求。
比如Object给我们准备了一个toString()方法,作用是“把对象翻译成字符串”。但这个通用版没有太多信息,默认只会告诉我们:这是哪个类、以及一个看不懂的编号。
好在 Java 允许我们“重写”:子类可以重新写一个名字相同、参数相同、返回值相同的方法,把从Object继承来的通用版本盖掉,换成更适合自己的版本。
重点记住一句话:继承来的方法,如果不满意,就可以重写它。
下面就用toString()来看具体怎么做。
默认效果
先写一个最普通的Student,里面只放两个信息:
publicclassStudent{Stringname;intage;}这时候我们没有重写任何方法,打印它:
Studentstu=newStudent();System.out.println(stu);会看到:
Student@1b6d3586这其实是Object默认的toString()输出:类名@地址编号(哈希值)。它知道“这是一个 Student 对象”,但不知道我们真正关心的是name和age。
重写 toString()
现在要求变清楚:我们希望打印学生时,能直接看到姓名和年龄。于是我们在Student里重写toString():
publicclassStudent{Stringname;intage;@OverridepublicStringtoString(){return"Student{name="+name+", age="+age+"}";}}解释两点:
@Override:这是 Java 的标记,意思是“我是在重写从父类继承来的方法,不是新写一个方法”。- 方法名、参数、返回值都和
Object里的toString()保持一致,才能真正覆盖它。
重写后,再打印同一个对象:
Student{name=小明, age=18}现在输出就是我们想要的信息了。
前后对比
- 不重写:用
Object给的通用版,只能看到Student@1b6d3586。 - 重写后:用
Student自己定制的版本,能直接看到name和age。
以后做动态数组时也会用同样的思路:给ZArrayList重写toString(),让打印整个数组时显示的是里面的元素,而不是一串看不懂的编号。