☰
Java形参与返回值:类、抽象类、接口怎么选?
2026/10/15 1:33:19 网站建设 项目流程

说实话,Java系列学到第十二天这个节点,挺微妙的。前面学了变量、循环、方法、对象,感觉自己好像啥都会了,一到"类名、抽象类名、接口名作为形参和返回值"这几个概念堆在一起,很多人就开始犯迷糊。day03这个题目我太熟了,当年学的时候也是一脸懵,后来带过不少新人,发现踩坑的点都集中在“能看懂、不会用”上面。

这篇就带你把这几个知识点串起来捋一遍。不是照着官方文档念定义,而是按照实际开发时候的思路,讲清楚这些东西到底是干嘛用的、怎么选、怎么写,特别是一些往常没人跟你细说的坑。内容不算难,但你认真看一遍,把代码自己敲一遍,后面学集合、学框架都会顺很多。

1. 类名、抽象类名、接口名作为形参和返回值

1.1 参数类型为什么能用"类",传的到底是啥

先聊最基础的一件事:Java方法的参数传递。你以前写的多半是这种:

public void sayHello(String name) { System.out.println("你好," + name); }

这是基本类型和String传参,大家都懂。但现在突然冒出“把类名当作形参类型”,意思就是说——你往方法里传的,不再是一个数字、一个字符串,而是一个实实在在的对象。

这里一定要搞明白一件事:Java里传对象,本质上传的是引用(理解为地址也行)。你用一句大白话说就是:方法拿到了对象的门牌号,顺着门牌号能改房子里的东西。

public class Person { public String name; public int age; } public void modifyPerson(Person p) { p.age = 30; // 顺着门牌号直接改属性 }

你在方法里改了属性,外面那个对象的age也跟着变了。这是很多新手绕不过去的弯:为什么我传了个对象进去,在外面它的值就被改了?因为传的是引用地址,不是复制了一份。

还有一个大家常犯的错——你在方法里把参数重新赋值了:

public void modifyPerson(Person p) { p = new Person(); // 这是把局部变量的指向改了,外面根本不知道 p.age = 99; }

这种操作纯属白费劲。参数变量和外面那个引用是两个"抽屉",只是里面存的地址一样。你把参数变量指向别处,外面那个引用还是原地址。所以记住了:通过参数修改对象内容,外面看得见;把参数变量指向新对象,外面不知道。

提示:刚上手时,先在纸上把"栈里存引用、堆里存对象"画一遍,很多疑惑会瞬间消失。

1.2 具体类名、抽象类名、接口名,三个级别的参数

现在把参数类型分成三种来看,这是day03标题的核心,也是整个Java多态的一个缩影。

第一种,具体类名。

比如方法签名是public void eat(Dog dog),你就必须传一个Dog对象,或者Dog子类的对象。这是最保守的写法,类型完全锁死。如果你今天要处理Cat、Bird,就得再写一个方法,代码会越写越像流水账。

public void eat(Dog dog) { ... } public void eat(Cat cat) { ... }

第二种,抽象类名。

如果写的是public void eat(Animal animal),这时候只要传一个Animal的子类对象就行,Dog、Cat都能装进来。抽象类参数的价值在于:你把“共性”定成了抽象类,方法只认这个共性,不管具体是谁。

但这里有个前提——如果每个子类行为差别很大,"共性"定义得不好,强行用抽象类反而别扭。抽象类适合那种"子类之间确实存在上下级关系,并且能抽出通用代码"的场景。

第三种,接口名。

接口参数是实战里出现频率最高的一种。public void doJob(Runnable task),传进来的只要实现了Runnable接口的东西,管你是Thread还是自定义Task,一律认。这就是面向接口编程的核心思想:方法不关心你具体是谁,只关心你能不能做到那件事。

给你个实际感受一下。在某公司的订单系统里,所有支付方式都实现同一个接口:

public interface PaymentStrategy { void pay(BigDecimal amount); } public class AlipayStrategy implements PaymentStrategy { public void pay(BigDecimal amount) { // 支付宝逻辑 } } public class WechatStrategy implements PaymentStrategy { public void pay(BigDecimal amount) { // 微信逻辑 } }

核心处理逻辑只需要写一次:

public void doPay(PaymentStrategy strategy, BigDecimal amount) { strategy.pay(amount); // 具体谁实现,运行时才知道 }

这就是多态最典型的应用。主流程代码纹丝不动,新接入一种支付方式就是新增一个实现类,绝不会去改主流程代码。后面你学Spring框架,这种思想更是铺天盖地。

1.3 返回值也能是类,三层逻辑同理

方法返回值写成类名、抽象类名、接口名,逻辑和形参是镜像关系。

你写public Dog getDog(),返回的必须是具体的Dog对象。你写public Animal getAnimal(),返回Dog、Cat都可以。你写public PaymentStrategy getStrategy(),返回任意支付实现类即可。

实际上,返回值用接口/抽象类的好处藏在一个特别重要的设计模式里——简单工厂模式。不用被名字吓到,说白了就是:把"该创建哪个对象"的判断,集中放到一个地方。

public static PaymentStrategy createStrategy(String type) { if ("ALIPAY".equals(type)) { return new AlipayStrategy(); } else if ("WECHAT".equals(type)) { return new WechatStrategy(); } throw new IllegalArgumentException("未知支付类型"); }

调用的人爽翻了:

PaymentStrategy strategy = PaymentFactory.createStrategy("ALIPAY"); doPay(strategy, BigDecimal.TEN);

调用者完全不用关心AlipayStrategy里面到底怎么写的,拿到一个接口引用就能干活。这种写法在真实项目里,一个模块几十上百个方法都会遵循这个套路,你越早习惯,看源码越不费劲。

1.4 主观上的三个选择建议

很多人在这一步纠结:我到底该用具体类、抽象类还是接口?

我的建议很简单。先看你有没有“多个不同类型的东西需要被同一个方法处理”。如果没有,老老实实用具体类,不要强行抽象,YAGNI原则在这里同样有效。如果有,再判断这些类型之间是严格的"is-a"亲属关系,且你需要抽取公共字段和公共代码——选抽象类。如果只是"有共同能力"的关系,比如能飞、能支付、能排序——选接口。Java单继承的痛点决定了,在大多数业务场景里,接口的出场率高得多。

2. 内部类:把相关的类装进"壳"里

2.1 内部类到底解决什么问题

内部类,字面意思就是在类的内部再定义一个类。你可能会问,好好的独立类不写,非要嵌在别的类里面干嘛?

我的理解是这样:有些类天生就是为另一个类服务的,它离开了宿主类就失去了存在的意义。比如电脑的主板类里,有一个用于控制电源的内部类,这个电源类不需要被外部其他地方使用,它只听主板的话。你把它单独放在外面,虽然也能跑,但从代码组织角度就多了一个"谁都可以用、但谁都不该乱用"的类。放在内部,语义清晰,封装性好,还能很方便地访问外部类的私有成员。

另外一个很实际的原因是写法简洁。特别是匿名内部类,在很多监听器、回调、多线程的场景里,你根本不想为一个一次性任务单独建一个类文件,内部类正好解决这个问题。

2.2 四种内部类各自的使用姿势

内部类在Java里一共四种,分别说。

成员内部类——定义在类里面、方法外面,和成员变量平级。它可以随意访问外部类的私有成员,包括私有变量、私有方法。创建方式有点绕,需要借助外部类对象:

public class Outer { private String secret = "内部机密"; class Inner { public void printSecret() { System.out.println(secret); } } }

外部创建成员内部类:

Outer outer = new Outer(); Outer.Inner inner = outer.new Inner(); inner.printSecret();

记住外部类对象.new 内部类()这个语法就行。在内部类里如果出现变量名冲突,用外部类名.this.成员来指外部类的成员。比如:

class Inner { private String secret = "内部自己的"; public void print() { System.out.println(Outer.this.secret); // 访问外部类的secret } }

静态内部类——定义时加static。它和成员内部类的最大区别是:不依赖外部类对象,可以直接new Outer.StaticInner()。但它不能直接访问外部类的实例成员,只能访问外部类的静态成员。说白了,它就是借了外部类的"命名空间",本质上是个独立类。因为不持有外部类引用,它也更安全,不容易造成内存泄漏。

局部内部类——定义在方法内部。这个类只在这个方法里有效,方法跑完它就没了。用处相对少,但有些时候你需要在方法内组织一段复杂的逻辑,又不想把它提升为类成员,就会用到。

匿名内部类——这是使用频率最高的一种。它本质上是"没有名字的局部内部类",在创建对象的同时直接定义类体。典型场景就是事件监听和线程:

Thread t = new Thread(new Runnable() { @Override public void run() { System.out.println("线程里的任务"); } }); t.start();

这里new的其实不是一个Runnable接口对象——接口根本不能new,这是创建了一个实现了Runnable接口的匿名类的对象。注意观察,new Runnable() { 类体 }这个写法,右边的大括号里就是我“临时造”出来的那个类的内容。

匿名内部类有几个细节要注意。第一,它只能被实例化一次。第二,从它诞生那刻起就要写完整实现,不适合复杂的逻辑。第三,它访问局部变量时,该变量必须是final(Java 8之后可以是effectively final,即变量值没有变化)。这是很多报错的来源:你在匿名内部类里修改了一个局部变量,编译直接给你报"Variable used in inner class must be final or effectively final"。

2.3 内部类和内存泄漏这个坑

这里必须认真说一个实战里特别容易踩的坑:非静态内部类持有外部类的引用。这意味着,如果某个生命周期很长的对象持有内部类实例,它就会一直“拽着”外部类对象不放,导致外部类无法被垃圾回收。在安卓这类内存敏感的环境里,这种问题尤为明显。

举个例子,某个工具类里有成员内部类,工具类本身是全局单例,里面又长期存了一个内部类实例,这个内部类又关联着外部类实例,那这个外部类就永远无法释放。

解决套路有几个:能不用非静态内部类就不用,改成静态内部类;如果内部类确实需要外部类数据,就把需要的数据当作参数传过来,降低耦合。我经常跟团队里的人说:没事别写非静态内部类,除非你非常清楚自己在干什么。

3. 常用API速查与实操心得

3.1 Math类:别自己造数学轮子

Math是一个所有方法都是静态的工具类,不需要new,直接用Math.xxx()就行。它的方法不多,但每个都很常用。

最基础的几个:Math.abs(-5)求绝对值;Math.max(3, 7)和Math.min(3, 7)取最大最小值;Math.pow(2, 10)算幂,注意返回值是double,2的10次方算出来是1024.0;Math.sqrt(9)开平方,返回3.0。

取整相关的有三个:Math.ceil(3.2)向上取整得到4.0;Math.floor(3.8)向下取整得到3.0;Math.round(3.5)四舍五入得到4。这三个API在写分页、计算平均值和金额换算的时候特别常用。

还有随机数,Math.random()生成一个[0.0, 1.0)之间的double随机值。常写的一个骚操作是生成某个范围内的随机整数:

// 生成 [min, max] 之间的随机整数 int num = (int)(Math.random() * (max - min + 1)) + min;

这行代码得能默写,面试和笔试都爱出。

Math类也有个容易忽略的点:Math.round的返回值。一个是Math.round(double)返回long,另一个是Math.round(float)返回int。如果你写的是Math.round(3.5f),用的是float版本;要是没加f,它是double版本。小数点后第一位是5的时候,结果是向上舍入的,这个细节在边界测试里容易翻车。

3.2 System类:时间戳和数组拷贝是真本事

System类虽然叫system,但它没有实例,也是全静态的。日常开发里常用的就这几个。

System.currentTimeMillis(),一听到这个名就想到"当前时间毫秒值",它返回从某个时间点(计算机纪元)到现在的毫秒数。这个API最朴素的应用是测量一段代码跑了多久:

long start = System.currentTimeMillis(); // 需要测量的代码 long end = System.currentTimeMillis(); System.out.println("耗时:" + (end - start) + "ms");

注意,这个毫秒值能用于计算时间差,但它不是自然的"时间格式化字符串",想要年月日时分秒还得配合Date、Calendar或者Java 8的LocalDateTime去转换。

System.arraycopy是另一个被很多人忽视但极其高效的方法。它专门用来拷贝数组,要注意理解它的5个参数:

System.arraycopy(源数组, 源数组起始位置, 目标数组, 目标数组起始位置, 拷贝长度);

比如:

int[] src = {1, 2, 3, 4, 5}; int[] dest = new int[5]; System.arraycopy(src, 1, dest, 0, 3); // 结果:dest = [2, 3, 4, 0, 0]

这个方法底层是native方法,性能比for循环拷贝好得多。你在很多框架源码里都能看到它的身影,比如ArrayList扩容的时候就是用它。

System.gc()它是提醒JVM执行垃圾回收,不一定是立刻执行,效果有限。写完垃圾回收的算法题或者排查内存问题的时候可能用到,但别把它当成万能按钮。

System.exit(0)直接结束JVM进程。参数0代表正常退出,非0代表异常退出。注意这方法在Web应用里几乎不要用,一旦调用整个进程就没了。

3.3 toString():为什么每个类都要重写它

toString()是谁都绕不开的方法,因为它是Object类里定义好的,所以每个类都有。但Object默认实现长这样:

public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); }

输出结果大致是com.demo.Person@1a2b3c4d。这对人来说没有任何可读性。所以如果你自己定了一个类,想打印出来看到属性信息,就需要重写toString()。

比如:

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

这样System.out.println(person)打印出来就是Person{name='张三', age=25},调试日志看着舒服多了。

实际上IDEA这类开发工具里有快捷键直接生成toString方法,一键就能帮你拼出这种字符串。但你要注意,在写POJO类的时候,我推荐用String.format或者StringBuilder来拼,避免大量字符串拼接产生过多的中间对象。虽然现代JVM对字符串拼接有优化,但复杂对象里字面量拼接太多,可读性也会下降。

日常开发里,toString还有个衍生玩法:把对象转成JSON输出,很多人会直接在toString里返回JSON格式,这在某些日志场景下比较方便。但注意别把日志打得太重,某些对象字段特别多,反复toString会影响性能。

3.4 Arrays类:数组干活的瑞士军刀

Arrays是一个专门操作数组的工具类,全是静态方法。它有四个操作我认为是必修中的必修。

Arrays.toString(数组),这个方法不是Object那个toString的重写,它专门把数组转成可读的字符串。你直接System.out.println(arr)打数组,出来的是[I@1a2b3c4d,因为数组也是一个对象,它调用的是Object的toString。但用了Arrays.toString(arr)就能输出[1, 2, 3, 4]这种格式。调试数组必备。

Arrays.sort(数组),直接原地排序。底层用的是双轴快速排序,对基本类型和引用类型都有优化,你不需要关心算法细节,直接用。最常踩的坑是:sort会改变原数组。所以如果你想保留原顺序,先复制一份再排。

Arrays.binarySearch(数组, 目标值),二分查找。注意前提是数组必须已经排好序,否则结果不可信。返回的是找到的下标;如果没找到,返回的是-(插入点) - 1,这个负数不是随便给的,它带信息。比如:

int[] arr = {1, 3, 5, 7}; int index = Arrays.binarySearch(arr, 4); // 返回 -3,意思是如果插入应该插在下标2的位置,也就是 -(2)-1 = -3

知道这个规律,你就可以通过返回值精确判断目标值应该插哪。

Arrays.copyOf(数组, 新长度)和Arrays.copyOfRange(数组, from, to),用于复制数组。copyOf在扩容场景特别常用,它会把不够的位置填充默认值。比如:

int[] arr = {1, 2, 3}; int[] bigger = Arrays.copyOf(arr, 5); // 结果:{1, 2, 3, 0, 0}

还有一个高密度坑:Arrays.asList很多人用,但不知道它返回的集合不能增删。它返回的是Arrays内部定义的一个固定长度List,底层还是数组。你要是调add()或者remove(),直接抛UnsupportedOperationException。要转成真正的ArrayList,得再包一层:new ArrayList<>(Arrays.asList(a))。这是一个面试题常客,也是平时跑着跑着突然炸出来的经典报错。

4. 冒泡排序:算法第一课,但你未必真懂

4.1 从生活场景理解冒泡原理

冒泡排序为什么叫冒泡?想象一缸水里有一堆气泡,气泡比水轻,所以会慢慢往上浮。大的气泡浮得快,最后到顶;小的浮得慢,沉在下面。冒泡排序就是这个逻辑:两两比较相邻元素,如果顺序不对就交换。每一轮下来,最大值就像最大的气泡一样"浮"到了数组末尾。

它的核心操作有三层:外层循环控制轮数,内层循环控制每一轮比较到哪,最里层做交换。排序的n个数,最多需要n-1轮,因为最后一轮只剩下一个数不用比。

4.2 经典版代码和优化版代码

先写最标准的实现,这段代码我在笔试里见过无数次:

public static void bubbleSort(int[] arr) { int n = arr.length; for (int i = 0; i < n - 1; i++) { for (int j = 0; j < n - 1 - i; j++) { if (arr[j] > arr[j + 1]) { int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } } }

注意内层循环的终止条件n - 1 - i,这减的i是关键。因为每一轮结束,最后一个元素已经归位,下一轮就不用再去碰它。不减去i也可以,但会白白比较很多次。

优化点来了。如果某一轮走下来,一次交换都没发生,说明数组已经有序,后面就没必要再比了。引入一个标志位:

public static void bubbleSort(int[] arr) { int n = arr.length; for (int i = 0; i < n - 1; i++) { boolean swapped = false; for (int j = 0; j < n - 1 - i; j++) { if (arr[j] > arr[j + 1]) { int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; swapped = true; } } if (!swapped) { break; } } }

优化效果对"基本有序"的数据特别明显。最好情况,数组一开始就有序,时间复杂度从O(n²)直接降到O(n)。最坏情况(倒序)仍然是O(n²)。

还有一种更稀少的"双向冒泡排序",每一轮同时把最大值带到尾、最小值带到头,可以减少一些轮数。但因为它bug多、边界条件复杂,面试写这个的性价比不高。我见过几个同学在考场上写双向冒泡,最后因为边界处理失误直接崩盘,反而丢了基础分。说白了,优化版单向冒泡足够了。

4.3 冒泡的稳定性、比较次数和真实评价

冒泡排序有个很重要的性质:稳定。什么叫稳定?如果有两个相等的元素,排序前后它们的相对顺序不变。因为冒泡比较时用了>而不是>=,所以相等元素不会交换,相对位置得以保留。

稳定性在真实的业务里很重要,比如你是先按时间排序,再按金额排序,第二次排序如果算法不稳定,时间上的相对顺序可能就被打乱了。所以面试官才总爱问"你知不知道哪些排序算法是稳定的"。

冒泡排序的时间复杂度:最好O(n)(优化版),最坏O(n²),平均O(n²)。空间复杂度O(1),因为它只用了几个临时变量,是原地排序。

说到评价,我在很多文章里看到有人说冒泡排序"没用、被淘汰了"。这话其实有点绝对。它教学价值极高,是理解比较类排序的入门钥匙;在小数据量(比如几十个数)的场景,它的性能差距完全可以忽略。Java标准库的Arrays.sort对基本类型用的双轴快速排序,对它引用类型用的TimSort,都不是冒泡,这没错。但你要是面试官,让我用三分钟考察一个候选人的基本编程能力,让他写个冒泡仍然很合适。所以我建议:别只会背代码,你得能讲清楚为什么内层循环要减i、为什么相等时不交换、时间复杂度怎么推出来的。

4.4 一个容易出错的冒泡变体题

挨着冒泡排序,有一种衍生题型特别坑:把"从大到小排序"也算冒泡变体,然后让新手上手就写反。我从多年的经验里,给一个建议的思考顺序:先确定你是找最大还是找最小腾到端点;再确定交换条件里的符号。

比如从大到小排,你应该把小的往尾部放,也就是当arr[j] < arr[j + 1]时交换。逻辑很简单,但考场上紧张起来,有人会把符号写成反的。别问我是怎么知道的。

还有一个容易忽略的点是数组越界。如果内层循环写的是j < n - 1而不是j < n - 1 - i,其实也能跑;但如果写的是j <= n - 1 - i,最后一轮比较arr[j + 1]就直接越界了。这种边界题是空指针、数组越界这类bug的高发地。

5. 实战演练:把这些知识点串成一个完整Demo

5.1 需求设定与思路拆解

为了让今天的内容不只是“听懂了”,我建议你亲手写一个这样的模拟Demo:设计一个支付系统,有三种支付方式,每一种有不同的手续费折扣;用一个工具方法统一执行支付;再用一个数组保存订单金额,用冒泡排序从小到大排好并打印出来。

这个需求虽然简单,但它几乎覆盖了今天的全部知识点:接口作为形参和返回值、匿名内部类或实现类、Arrays排序、toString、时间戳测量。

5.2 代码与运行过程讲解

先定义一个支付接口:

public interface PaymentStrategy { double calculateFee(double amount); }

两个实现类:

public class CreditCardStrategy implements PaymentStrategy { private double rate = 0.03; @Override public double calculateFee(double amount) { return amount * rate; } @Override public String toString() { return "CreditCardStrategy{rate=" + rate + "}"; } } public class BankTransferStrategy implements PaymentStrategy { private double rate = 0.01; @Override public double calculateFee(double amount) { return amount * rate; } @Override public String toString() { return "BankTransferStrategy{rate=" + rate + "}"; } }

写一个工厂,返回接口类型:

public class PaymentFactory { public static PaymentStrategy create(String type) { if ("CREDIT".equalsIgnoreCase(type)) { return new CreditCardStrategy(); } else if ("BANK".equalsIgnoreCase(type)) { return new BankTransferStrategy(); } return new PaymentStrategy() { // 匿名内部类兜底 @Override public double calculateFee(double amount) { return 0.02 * amount; } }; } }

再开一个主流程,做调用和排序:

public class Main { public static void main(String[] args) { long start = System.currentTimeMillis(); PaymentStrategy strategy = PaymentFactory.create("CREDIT"); double amount = 1000.0; System.out.println("手续费:" + strategy.calculateFee(amount)); // 模拟订单金额数组 int[] amounts = {58, 23, 99, 12, 67}; bubbleSort(amounts); System.out.println("排序后:" + Arrays.toString(amounts)); long end = System.currentTimeMillis(); System.out.println("整体耗时:" + (end - start) + "ms"); } public static void bubbleSort(int[] arr) { int n = arr.length; for (int i = 0; i < n - 1; i++) { boolean swapped = false; for (int j = 0; j < n - 1 - i; j++) { if (arr[j] > arr[j + 1]) { int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; swapped = true; } } if (!swapped) break; } } }

把这段代码敲一遍,你会看到接口引用、工厂返回接口、匿名内部类、Arrays.toString、冒泡排序、System.currentTimeMillis全都跑了一遍。建议你自己再改一改:把工厂返回值类型改成Object看看会出现什么问题,或者在匿名内部类里修改外部局部变量看编译报错长什么样。这些“错误体验”记得越牢,动手能力长得越快。

6. 常见问题与排查技巧实录

6.1 编译报错汇总

学习阶段遇到编译错误不要慌,下面几个是高频中的高频,我做了个速查表:

报错信息原因解决方法
NullPointerException调用了null引用上的方法打印或检查对象是否真的传入了
Variable used in inner class must be final or effectively final匿名内部类访问了后续被修改的变量重新定义一个最终变量,或者用数组/包装类绕过
UnsupportedOperationException对Arrays.asList结果调用add/remove用new ArrayList<>(Arrays.asList(...))
ArrayIndexOutOfBoundsException冒泡内层循环边界没算对检查j < n - 1 - i是否满足

其中“effectively final”这个限制,是一个特别反直觉的东西。你想在匿名内部类里改一个变量计数,编译器不允许。正确的想法不是去打破这个限制,而是换个思路:可以定义一个长度为1的数组,在内部类里改数组元素,因为数组对象本身没有被重新赋值,只是改它的内容,这样编译就通过了。不过这个写法比较trick,在真实项目里见到别惊讶。

6.2 面试官常追问的连环问题

我把这一节的内容在模拟面试里总结成了几个连环问,你如果都能答上来,说明今天的内容八成是吃透了。

面试官问:接口能不能new?答案是不能。但new Runnable(){...}看起来像new接口,实际是new了一个实现该接口的匿名类。

再问:匿名内部类和局部内部类有什么区别?匿名内部类没有类名,创建的同时直接实例化;局部内部类有名字,可以先定义再在方法里多次实例化。

又问:Arrays.sort底层用的什么排序?不同JDK版本有差异,Java 8里对基本类型用的是双轴快速排序,对引用类型用的是归并排序的变体TimSort。注意基本类型排序不稳定,引用类型排序稳定。

还问:为什么重写equals必须要重写hashCode?这俩曹植和曹丕的关系在Java里是约定好的:两个对象equals相等,hashCode必须相等;hashCode相等,equals不一定相等。如果你破坏了这条规则,HashMap、HashSet这些哈希结构的集合就会出大问题,出现"我明明放了,却取不出来"的诡异现象。

6.3 我能给你的两个长期建议

第一个建议,把今天的代码全部手敲一遍,不要复制粘贴。你看着代码和自己动手打,是完全不同的记忆强度。特别是匿名内部类那个大括号的位置、冒泡排序的边界,不亲手敲三遍以上,你考试的时候一定写不利索。

第二个建议,找一个小项目,把"接口参数+返回接口+匿名内部类+Arrays+toString"这套组合故意用一遍。比如写一个简单的计算器插件系统,定义一个Operation接口,有加法、减法实现类,一个工厂创建它们,再用数组存储一批数字,用冒泡排序,最后打印。这个组合练熟了,后面接触Spring之类框架时,你会觉得很多概念似曾相识。

我在实际带人时发现,day03这个阶段最容易发生的现象是“眼睛会了,手不会”。课堂上看着代码觉得啥都懂,一关编辑器全白瞎。所以你也千万别眼睛会了就觉得会了,一定要动手。做到什么程度算过关?给你一张白纸,能把接口参数的方法签名写出来,能把冒泡排序无bug写出来,今天就算真的值回票价了。

最后再分享一个小技巧:学这类语言特性的时候,每学一个概念,就在自己的代码笔记里记一行“在XX项目/场景中,我用它解决了什么问题”。哪怕暂时没遇到问题,也写一句“它大概会在XX场景用到”。这能帮你把知识从课本搬运到实战,后面找工作、做项目的时候,这些笔记会变成你的底气。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询