面向对象高级(代码块、内部类、函数式编程、常用API)

一、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();
    
  • 访问特点
    1. 成员内部类中可以直接访问外部类的实例成员和静态成员。
    2. 如果在内部类的实例方法中,发生了变量名冲突,可以使用 外部类名.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 提供了极其变态的省略规则:

  1. 参数类型可以全部省略不写
  2. 如果只有一个参数,参数类型省略的同时,外面的小括号 () 也可以省略
  3. 如果大括号 {} 里面只有一行代码,大括号 {} 可以省略,最后的分号 ; 也可以省略
  4. 【易错点】如果在省略 {} 的情况下,这唯一的一行代码是 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:用来替代普通数组的动态容器,完美解决数据量大小不确定的业务场景。只需熟记 addgetsizeremove 即可走遍天下。
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容