一、Java 面向对象高级:代码块 (Code Blocks) 秒懂指南
在 Java 中,类的五大成分包括:成员变量、构造器、方法、代码块、内部类。今天我们将深入探讨其中比较特殊的一个成分——代码块。
代码块分为两种核心类型:静态代码块和实例代码块。它们在执行时机和作用上有着明确的区别。
1. 静态代码块 (Static Block)
-
语法格式:
```java
static {
// 静态代码块内容
}
``` - 执行特点:类加载时自动执行。由于在 Java 的运行机制中,一个类通常只会加载一次,所以静态代码块也只会执行一次。
- 核心作用:主要用于完成类的初始化操作。例如:对静态变量进行统一的初始化赋值,或者在类加载时加载一些全局的配置文件。
2. 实例代码块 (Instance Block)
-
语法格式:
```java
{
// 实例代码块内容
}
``` - 执行特点:每次创建对象(new)时都会执行,并且它的执行时机会在构造器之前。
- 核心作用:和构造器一样,都是用来完成对象的初始化的。当你发现多个构造器中有大量重复的初始化代码时,就可以把这些重复代码提取到实例代码块中,例如:对实例变量进行统一的初始化赋值。
二、 Java 面向对象高级:内部类 (Inner Class) 全景指南
在 Java 开发中,如果一个类定义在另一个类的内部,这个类就是内部类。
为什么要用内部类?
当一个类的内部包含了一个完整的事物,且这个事物没有必要单独在外面设计一个文件时,就可以把它设计成内部类。比如汽车(Car)类内部包含一个引擎(Engine)类。
在 Java 中,内部类主要分为四种:成员内部类、静态内部类、局部内部类和匿名内部类。今天我们来逐一盘点。
1. 成员内部类
成员内部类就像是外部类中的一个普通成员变量或普通成员方法。
-
创建对象的格式:必须先有外部类对象,才能有内部类对象。
外部类名.内部类名 对象名 = new 外部类().new 内部类(); // 例如:Outer.Inner in = new Outer().new Inner(); -
访问特点:
- 成员内部类中可以直接访问外部类的实例成员和静态成员。
- 如果在内部类的实例方法中,发生了变量名冲突,可以使用
外部类名.this拿到当前外部类对象。
2. 静态内部类
有 static 修饰的内部类,它属于外部类自己持有,而不是属于外部类的某个具体对象。
-
创建对象的格式:不需要先创建外部类对象,直接通过外部类名调用。
外部类名.内部类名 对象名 = new 外部类.内部类(); // 例如:Outer.Inner in = new Outer.Inner(); - 访问特点:因为它是静态的,所以只能直接访问外部类的静态成员,绝对不可以直接访问外部类的实例成员。
3. 局部内部类(鸡肋语法,了解即可)
局部内部类是定义在方法中、代码块中或者构造器等执行体中的类。它的作用域仅限于它被定义的那个代码块内部。在现代企业开发中,我们极少使用这种语法。
4. 匿名内部类(👑 绝对核心,开发必用!)
匿名内部类是一种特殊的局部内部类,顾名思义:程序员不需要为这个类声明名字。
- 它的本质:匿名内部类本质上是一个子类(或者接口的实现类),并且代码执行到那里时,会立即创建出一个该子类的对象。
- 基本作用:用来极其方便地创建一个子类对象,省去了单独写一个实现类的麻烦。
语法格式:
new 类或接口(参数值...) {
// 类体 (一般是重写方法)
};
实战场景:作为对象参数传递
在调用别人提供的方法时,如果该方法需要一个接口或抽象类的实现对象,我们就可以直接抛一个匿名内部类进去。
public interface Swimming {
void swim();
}
public class Test {
public static void main(String[] args) {
// 直接传递匿名内部类对象,无需再单独建一个 Student 类去实现 Swimming 接口
go(new Swimming() {
@Override
public void swim() {
System.out.println("学生游泳飞快~~~");
}
});
}
public static void go(Swimming s) {
s.swim();
}
}
补充场景:
在 Java GUI 编程中为按钮绑定点击事件(ActionListener),或者在使用集合排序方法(Arrays.sort)时传入比较器规则(Comparator),匿名内部类都是无可替代的最佳选择(尽管在 JDK 8 之后,它可以进一步被 Lambda 表达式简化)。
三、深度解析:匿名内部类在数组排序中的实战应用
在 Java 开发中,我们经常需要对自定义对象数组(如学生、员工)进行排序。这时,Arrays.sort 配合匿名内部类是最经典的实现方式。
下面我们将对这段代码进行拆解分析。
1. 代码核心结构
这段代码的主要目的是调用 Arrays.sort 方法对 students 数组进行排序。由于 Student 是自定义类,Java 不知道该按什么规则排序,因此需要我们传入一个“比较器”。
关键语法:匿名内部类
new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) { ... }
}
-
本质:这行代码实际上创建了一个实现了
Comparator接口的匿名子类对象。 -
优势:我们不需要专门写一个
StudentComparator类,直接在调用的地方原地定义规则,非常方便。
2. 排序逻辑解析:升序与降序到底怎么写?
Comparator 接口的核心是 compare(Student o1, Student o2) 方法。
这里的 o1 代表前面的元素,o2 代表后面的元素。
底层逻辑是根据你返回的数字正负来决定要不要交换位置:
- 返回正整数:认为左边对象(o1)大于右边对象(o2),需要交换位置。
- 返回负整数:认为左边对象(o1)小于右边对象(o2),不交换,保持原样。
- 返回 0:认为两者相等。
⬆️ 升序写法(从小到大排)
图片代码中使用了 return o1.getAge() - o2.getAge();,这就是标准的升序写法。
-
必背口诀:前者减后者 (
o1 - o2) -
原理推导:如果
o1(前)大于o2(后),相减得正数。Java 规定正数代表需要交换,所以大的数字就被换到后面去了,最终形成了从小到大的升序。
⬇️ 降序写法(从大到小排)
如果你想让年龄大的排在前面,只需要把相减的顺序反过来写成:return o2.getAge() - o1.getAge();
-
必背口诀:后者减前者 (
o2 - o1) -
原理推导:如果
o2(后)大于o1(前),用o2 - o1就会得到正数。正数代表需要交换,所以大的数字(o2)就被换到了前面,最终形成了从大到小的降序。
3. 为什么这是匿名内部类的“典型场景”?
在开发中,当我们调用别人提供的方法(如 Arrays.sort),而这个方法正好要求传输一个对象参数,且该对象只需要用这一次时,匿名内部类就是最佳选择。
- 临时性:排序规则可能只在这个方法里用一次。
- 简洁性:减少了类文件的创建。
四、Java 函数式编程秒懂:Lambda 表达式与方法引用
在 JDK 8 之前,Java 在处理某些简单逻辑(比如集合排序、按钮点击事件)时,必须得写一大坨“匿名内部类”代码。代码臃肿且可读性差。
为了解决这个问题,JDK 8 引入了一种全新的语法——Lambda 表达式,标志着 Java 正式拥抱“函数式编程”。它的核心思想是:强调做什么,而不是以什么形式去做。
1. 使用 Lambda 的核心前提:函数式接口
⚠️ 铁律:Lambda 表达式只能用于替代“函数式接口”的匿名内部类!
什么是函数式接口?
- 有且仅有一个抽象方法的接口。
- 通常,我们会在这种接口上方打上
@FunctionalInterface注解。这个注解的作用是让编译器帮忙检查,确保里面真的只有一个抽象方法。
2. Lambda 的基础语法与省略规则
Lambda 表达式的标准格式非常像数学里的函数推导:
(被重写方法的形参列表) -> { 方法体代码; }
初级简化:替代匿名内部类
假设我们要对学生数组进行排序,传统的匿名内部类是这样的:
Arrays.sort(students, new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return o1.getAge() - o2.getAge();
}
});
由于 Comparator 是函数式接口,我们用 Lambda 直接改写:
Arrays.sort(students, (Student o1, Student o2) -> {
return o1.getAge() - o2.getAge();
});
终极简化:Lambda 的“神仙省略规则”
为了追求极致的优雅,Java 给 Lambda 提供了极其变态的省略规则:
- 参数类型可以全部省略不写。
- 如果只有一个参数,参数类型省略的同时,外面的小括号
()也可以省略! - 如果大括号
{}里面只有一行代码,大括号{}可以省略,最后的分号;也可以省略。 -
【易错点】如果在省略
{}的情况下,这唯一的一行代码是return语句,那么必须把return关键字一起省略掉!
按照上述规则,我们的排序代码最终会变成惊人的一行:
// 极致优雅的最终形态
Arrays.sort(students, (o1, o2) -> o1.getAge() - o2.getAge());
3. 进阶:方法引用 (Method References)
如果 Lambda 表达式的方法体里,仅仅只是在调用一个已经存在的方法,我们还可以用双冒号 :: 进一步简化,这就是方法引用。
方法引用分为 4 种常见形式:
① 静态方法引用:类名::静态方法
场景:如果 Lambda 表达式里只是调用一个静态方法,并且 -> 前后参数的形式一致。
// Lambda 写法
Arrays.sort(students, (o1, o2) -> CompareUtils.compareAge(o1, o2));
// 方法引用写法
Arrays.sort(students, CompareUtils::compareAge);
② 实例方法引用:对象名::实例方法
场景:如果 Lambda 表达式里只是通过某个现成的对象名称调用其方法,并且参数情况一致。
CompareUtils util = new CompareUtils();
// Lambda 写法
Arrays.sort(students, (o1, o2) -> util.compareAge(o1, o2));
// 方法引用写法
Arrays.sort(students, util::compareAge);
③ 特定类型的方法引用:特定类的名称::方法
场景:如果 Lambda 参数列表中的第一个参数是作为主调对象,后面的所有参数都是传给这个主调对象的入参,就可以直接用特定类名来引用!
// Lambda 写法(o1 调方法,把 o2 当参数传进去)
Arrays.sort(strings, (o1, o2) -> o1.compareToIgnoreCase(o2));
// 方法引用写法
Arrays.sort(strings, String::compareToIgnoreCase);
④ 构造器引用:类名::new
场景:如果 Lambda 表达式里只是在 new 创建对象,并且前后参数一致。
// 假设我们要创建一个能将字符串转换为对象的工厂
// Lambda 写法
CreateUserFactory factory = (name) -> new User(name);
// 构造器引用写法
CreateUserFactory factory = User::new;
总结
-
Lambda 表达式
->用于极大地简化匿名内部类的冗余代码,前提是接口只能有一个抽象方法。 - 省略规则:类型可省,单参去括,单行去大括去分号去 return。
-
方法引用
::是 Lambda 的进一步简化,追求代码极致的整洁与复用。
4. 进阶:如何进一步简化?
根据函数式编程的思想,由于 Comparator 是一个函数式接口(只有一个抽象方法),这段代码在 JDK 8 以后可以利用 Lambda 表达式 极大简化:
// 简化后的优雅写法(升序)
Arrays.sort(students, (o1, o2) -> o1.getAge() - o2.getAge());
// 如果想要降序,改成这样即可:
Arrays.sort(students, (o1, o2) -> o2.getAge() - o1.getAge());
这种写法去掉了冗余的 new Comparator 和方法签名,可读性更强,更加“优雅”。
五、Java 常用 API 秒懂:String 与 ArrayList 核心梳理
在 Java 开发中,API(Application Programming Interface)就像是 JDK 提前为我们造好的“工具箱”。我们不需要自己从零开始写底层逻辑,直接拿来用即可。
在众多 API 中,最常用、面试最常考的莫过于 String(字符串)和 ArrayList(集合)了。今天我们就来快速盘点它们的核心用法。
1. String:无处不在的字符串
String 代表字符串,它的对象可以封装字符串数据,并提供了极其丰富的方法来完成对字符串的处理(例如:登录系统的账号密码校验、随机验证码的生成等场景)。
① 创建字符串对象的两种姿势
-
方式一(最推荐):直接使用双引号。
String name = "黑马程序员"; -
方式二:通过
new String调用构造器。-
public String():创建一个空白字符串对象 -
public String(String original):根据传入的字符串内容创建 -
public String(char[] chars):根据字符数组的内容创建 -
public String(byte[] bytes):根据字节数组的内容创建
-
② 必须背熟的常用方法
| 方法名 | 核心说明 |
|---|---|
length() |
获取字符串的长度(即字符个数) |
charAt(int index) |
获取某个索引位置处的字符 |
toCharArray() |
将当前字符串转换成字符数组返回 |
equals(Object anObject) |
判断内容是否一样(切记:字符串内容比较千万别用 ==) |
equalsIgnoreCase(String another) |
判断内容是否一样(忽略大小写,常用于图形验证码比对) |
substring(int begin, int end) |
截取字符串(核心规则:包前不包后) |
replace(CharSequence target, CharSequence replacement) |
使用新值替换字符串中的旧值(常用于屏蔽敏感词) |
contains(CharSequence s) |
判断字符串中是否包含了特定的某段字符串 |
split(String regex) |
按照某个内容分割字符串,并返回字符串数组 |
2. ArrayList:大小可变的“超级数组”
很多初学者会问:既然有了数组,为什么还要学集合?
核心原因在于:数组定义完成并启动后,长度就固定了。而在实际开发中(比如购物车添加商品、企业人事管理系统增加员工),数据的数量往往是动态变化的。
集合的大小可变,功能更丰富,是开发中的绝对主力容器。而 ArrayList 就是集合家族中最常用的一种。它是一个泛型类,可以通过 <E> 来约束内部存储的数据类型。
① 创建集合对象
// 创建一个只能装 String 类型的集合
// 后面的 <> 内部类型可以省略不写(JDK 7+ 特性)
ArrayList<String> list = new ArrayList<>();
② 核心增删改查(CRUD)方法
| 方法名 | 核心说明 |
|---|---|
add(E e) |
将指定的元素添加到此集合的末尾 |
add(int index, E element) |
在此集合的指定位置插入指定的元素 |
get(int index) |
返回指定索引处的元素 |
size() |
返回集合中元素的个数(易错点:数组求长度用 .length 属性,集合求大小用 .size() 方法) |
remove(int index) |
删除指定索引处的元素,并返回被删除的元素 |
remove(Object o) |
删除指定的第一个匹配元素,返回是否删除成功 (true/false) |
set(int index, E element) |
修改指定索引处的元素,并返回被覆盖的旧元素 |
总结
- String:用来封装和处理字符串,创建方式首选直接赋值(双引号),需要熟练掌握截取、比较、替换等文本处理方法。
-
ArrayList:用来替代普通数组的动态容器,完美解决数据量大小不确定的业务场景。只需熟记
add、get、size、remove即可走遍天下。