内部类

内部类就是类中再套一个类

eg:

1
2
3
4
class Outer {
class Inner {
}
}

作用:

让一个类和另一个类关系更紧密

内部类可以方便访问外部类的成员

有些场景下写起来更方便,比如事件处理、临时小对象

Java 里常见有 4 种:

  • 成员内部类
  • 静态内部类
  • 局部内部类
  • 匿名内部类

成员内部类

就是:像成员变量一样,直接写在类里面,不加 static。

1
2
3
4
5
6
7
8
9
class Outer {
private String name = "Outer";

class Inner {
public void show() {
System.out.println(name);
}
}
}

这里 Inner 可以直接访问 Outer 的私有成员 name。

创建对象

成员内部类对象不能直接 new,一般要先有外部类对象:

1
2
3
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.show();

记忆点

成员内部类 依附于外部类对象存在。

静态内部类

就是:内部类前面加 static。

1
2
3
4
5
6
7
8
9
class Outer {
static String country = "China";

static class Inner {
public void show() {
System.out.println(country);
}
}
}

创建对象

静态内部类不需要依赖外部类对象:

1
2
Outer.Inner inner = new Outer.Inner();
inner.show();

特点

它只能直接访问外部类的 静态成员,不能直接访问非静态成员。

如果想访问非静态成员,得先创建外部类对象。

局部内部类

就是:定义在方法里面的类。

1
2
3
4
5
6
7
8
9
10
11
12
class Outer {
public void test() {
class Inner {
public void show() {
System.out.println("局部内部类");
}
}

Inner inner = new Inner();
inner.show();
}
}

特点

  • 只能在当前方法里使用
  • 作用域只在这个方法内部

匿名内部类

没有名字的内部类,通常只用一次。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Animal {
void speak();
}

public class Test {
public static void main(String[] args) {
Animal a = new Animal() {
@Override
public void speak() {
System.out.println("汪汪");
}
};

a.speak();
}
}

这里:

1
2
3
4
5
6
new Animal() {
@Override
public void speak() {
System.out.println("汪汪");
}
}

就是匿名内部类。

它本质上做了什么

相当于:

  • 创建了一个实现 Animal 接口的类
  • 这个类没有名字
  • 顺手直接创建了这个类的对象

四种内部类快速对比

成员内部类

定义在类里,不加 static

1
2
3
class Outer {
class Inner {}
}

特点:依赖外部类对象


静态内部类

定义在类里,加 static

1
2
3
class Outer {
static class Inner {}
}

特点:不依赖外部类对象


局部内部类

定义在方法里

1
2
3
void test() {
class Inner {}
}

特点:只能在方法里用


匿名内部类

没有类名,通常只用一次

1
2
3
4
new Runnable() {
@Override
public void run() {}
};

特点:简洁,常用于临时实现接口或继承类

Lambda

为什么会出现 Lambda

在 Java 8 以前,很多场景都要写匿名内部类,特别啰嗦。

比如线程:

1
2
3
4
5
6
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
};

这段代码真正重要的逻辑其实只有一句:

1
System.out.println("hello");

但为了让 Java 知道“你是在实现 Runnable 接口的 run() 方法”,你得写很多模板代码。

所以 Java 8 引入 Lambda,把它简化成:

1
Runnable r = () -> System.out.println("hello");

一下子就清爽很多。

Lambda 只能用于什么地方

Lambda 只能用于函数式接口。

函数式接口就是:

有且只有一个抽象方法的接口。

例如:

1
2
3
interface MyRunnable {
void run();
}

这就是函数式接口。

可以有别的方法,但只能有 一个抽象方法。

比如:

1
2
3
4
5
6
7
8
9
10
11
12
@FunctionalInterface
interface Test {
void run(); // 唯一抽象方法

default void hello() {
System.out.println("hello");
}

static void hi() {
System.out.println("hi");
}
}

这里仍然是函数式接口,因为:

  • run() 是抽象方法
  • default 方法不是抽象方法
  • static 方法也不是抽象方法

@FunctionalInterface

1
2
3
4
@FunctionalInterface
interface Calculator {
int add(int a, int b);
}

它的作用是:

  • 告诉别人:这是个函数式接口
  • 编译器会帮你检查:如果你不小心写了两个抽象方法,会直接报错

Lambda 的基本语法

Lambda 的核心语法是:

1
(参数列表) -> { 方法体 }

比如:

1
(a, b) -> a + b

意思就是:

  • 接收两个参数 a、b
  • 返回 a + b

eg:

1
2
3
interface Calculator {
int add(int a, int b);
}

Lambda 写法:

1
2
3
Calculator c = (a, b) -> {
return a + b;
};

如果方法体只有一句 return,可以继续简化成:

1
Calculator c = (a, b) -> a + b;

Lambda 可以访问外部变量吗

它可以访问:

  • 外部类成员变量
  • 静态变量
  • 局部变量,但局部变量必须是 final 或 effectively final(你虽然没写 final,但它的值没有再改过)

方法引用

普通 Lambda:

1
list.forEach(s -> System.out.println(s));

方法引用:

1
list.forEach(System.out::println);

不是所有 Lambda 都能改成方法引用。

只有当 Lambda 逻辑本质上是:

  • 拿到参数
  • 直接调用一个已有方法
  • 没有额外加工

才可以改。

比如这个可以:

1
s -> Integer.parseInt(s)

可以写成:

1
Integer::parseInt

但这个一般就不能直接改:

1
s -> s.trim().toUpperCase()

因为里面做了两步,不是单纯调用一个方法。

语法

1
2
3
4
System.out::println
Integer::parseInt
String::length
ArrayList::new

分别对应:

  • 对象实例方法
  • 静态方法
  • 类名引用实例方法
  • 构造器引用

Stream API

Java 8引入了Stream API,它提供了一种高效且易于使用的数据处理方式,特别适合集合对象的操作,如过滤、映射、排序等。

它不存数据,只负责“加工数据”

1
2
3
4
5
6
List<String> list = Arrays.asList("Java", "Go", "Python");

list.stream()
.filter(s -> s.length() > 2)
.map(String::toUpperCase)
.forEach(System.out::println);

这段代码的意思是:

  1. 把 list 变成流
  2. 过滤长度大于 2 的字符串
  3. 转成大写
  4. 输出

特点

  • 写法更简洁
  • 适合链式操作
  • 常和 Lambda 一起用

基本流程

1
2
3
数据源.stream()
.中间操作()
.终止操作();

常见中间操作

  • filter(按条件筛选元素)
  • map(把一个元素转成另一个元素)
  • sorted(排序)
  • distinct(去重)

常见终止操作

  • forEach(遍历输出)
  • collect(收集结果)
  • count(统计项目)
  • findFirst(找第一个)

并行流

普通流:

1
list.stream()

并行流:

1
list.parallelStream()

或者:

1
list.stream().parallel()

普通 stream() 是单线程顺序处理。
并行 parallelStream() 是多线程处理

image-20260409201255728

对CPU密集型的任务来说,并行流使用ForkJoinPool线程池,为每个CPU分配一个任务,这是非常有效率的,但是如果任务不是CPU密集的,而是I/O密集的,并且任务数相对线程数比较大,那么直接用ParallelStream并不是很好的选择。

Java基础

Java特点

img

平台无关性:编译成字节码后能在任何装了jvm的机器上运行

面向对象

内存管理:Java有自己的垃圾回收机制,能自动管理对象,减少内存泄漏

JVM、JDK、JRE三者关系?

image-20240725230247664

值传递和引用传递

在 Java 中,参数传递只有值传递一种方式

基本类型(int、double、boolean 等) 传的是值本身的副本,方法内改动不影响外部变量。

引用类型(对象、数组) 传的是对象引用的副本(即对象的地址值拷贝),不是对象本身。

面向对象三大特性

封装:把数据和操作数据的方法绑定在一起,对外隐藏内部实现细节,只暴露必要的访问接口。

继承:子类继承父类的属性和方法,实现代码复用。

多态:同一个方法调用,在不同对象上表现出不同行为。提高扩展性。

多态实现方式

  1. 继承或实现接口(有继承关系)
  2. 方法重写(子类重写父类方法)
  3. 父类引用指向子类对象(向上转型)

== 与 equals 有什么区别?

基本数据类型==比较数值;

引用类型:==和equals都是比较内存地址,但是用的类(比如 String、Integer)都重写了equals,重写后比较的是对象里面的内容。

重写equals必须重写hashcode

如果两个对象equals相等,那么hashcode必须相等

如果没重写hashcode,equals判定为true,但是hashcode不一样,放到hashse或者HashMap中会被当成两个不同元素,造成去重失效。

接口和抽象类

类只能单继承抽象类,可以实现多个接口

抽象类能有构造器,普通成员方法和任意权限成员变量;

接口默认成员变量是public static final,Java8后能有默认,静态方法,Java9还加了私有方法

抽象类能不能有构造方法:能。只是不能new抽象类。抽象类构造方法能初始化内部的公共成员变量。

final修饰变量

基本类型:不能改值

引用类型:不能再让变量指向另一个对象,但对象内部的数据可能仍然可以修改

error和异常的区别

**error:**无法处理的严重问题,jvm不可恢复(堆,栈溢出)

Exception: 表示程序本身可以处理的异常情况

受检异常,Checked Exception,编译时就必须被捕获或者声明抛出

非受检异常,Unchecked Exception 或 RuntimeException,运行时异常是不需要在编译时强制捕获或声明的

string为什么不可变

string类被final修饰,不能被继承重写

底层数组是private final 修饰的数组引用

创建string对象的两种方式:

new String()

字面量赋值

说一下 integer的缓存

Integer 缓存:值相同,对象未必相同

Integer 用自动赋值时,数值在 - 128 到 127 之间,多个变量会共用同一个对象,用 == 比地址会相等;一旦数值超过 127,就会直接 new 新对象,哪怕数值一样,== 也会不等,但用 equals 比值就没问题。

方法重载与重写

同一个类,方法名相同,参数列表不同,和返回值、访问修饰符无关

父子类,方法名、参数列表、返回值必须一致

throw和throws

throws(方法声明上)

  • 位置:方法签名后
  • 作用:声明该方法可能抛出异常,交给调用者处理
  • 可以多个异常,逗号隔开
1
void test() throws IOException, RuntimeException {}

throw(方法体内)

  • 位置:方法内部
  • 作用:手动抛出一个具体异常对象
1
throw new NullPointerException();

final / finally / finalize

final

修饰符

  1. 类:不能被继承
  2. 方法:不能重写
  3. 变量:常量,只能赋值一次

finally

异常块 try-catch-finally

  • 写在catch后面
  • 无论是否异常,都会执行
  • 常用于释放资源

finalize

Object里的方法

  • JVM垃圾回收前尝试调用,用来清理资源
  • 不保证一定执行,时机不确定,已废弃,不要用