Java笔记

Java版本

Java SE 版本 JDK版本 发布时间 开发代号
Oak 1995-05-23 Oak(橡树)
Java 1.0 JDK 1.0 1996-01-23
Java 1.1 JDK 1.1 1997-02-18
J2SE 1.2 JDK 1.2 1998-12-04 Playerground(运动场)
J2SE 1.3 JDK 1.3 2000-05-08 Kestrel(美洲红隼)
J2SE 1.4 JDK 1.4 2002-02-13 Merlin(灰背隼)
Java SE 5.0 JDK 1.5 2004-09-29 Tiger(老虎)
Java SE 6 JDK 1.6 2006-12-11 Mustang(野马)
Java SE 7 JDK 1.7 2011-07-28 Dolphin(海豚)
Java SE 8 JDK 1.8 2014-03-18 Spider(蜘蛛)
Java SE 9 JDK 1.9 2017-09-21
Java SE 10 JDK 10 2018-03-21
Java SE 11 JDK 11 2018-09-25
Java SE 12 JDK 12 2019-03-20

static

详见Java中的static

面向对象

父类中的方法默认可以被子类重写,加上final则只能被继承不能被重写。而加了final的类无法被继承。

父类的构造方法不能被继承,但子类会调用父类的构造方法。Java虚拟机构造子类对象前会先构造父类对象,父类对象构造完成之后再来构造子类特有的属性,这被称为内存叠加。而Java虚拟机构造父类对象会执行父类的构造方法,所以子类构造方法必须调用super()即父类的构造方法。如果子类的构造方法中没有显示地调用父类构造方法,则系统默认调用父类无参数的构造方法。

子类重写父类的方法,作用域不能比父类小。如Son重写Father的output方法,该方法在Father中是protected的,那么Son中重写只能是public或protected,不能是default和private。

只有抽象类中才能有抽象方法,抽象方法不能有函数体,非抽象的子类必须实现其函数体。

基本类型 和 包装类

数据类型

有时我们需要基本数据类型也有对象的特征,因此出现了包装类,将基本数据类型包装起来使其具有对象的性质,并添加了属性和方法。

基本数据类型与包装类型的区别

  • 默认值不同:基本类型的值是0false等,包装类默认为null
  • 初始化方式不同:包装类要用new的方式创建,基本类型不需要
  • 存储方式不同:基本类型主要保存在栈上,包装类对象保存在堆上

== 和 equals()

对于基本数据类型如,==比较的是值,且基本类型没有equals方法。

对于包装类等对象,==比较的是地址,equals方法默认比较的也是地址,但可以重写来自定义比较逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public static void main(String[] args) {
Integer a = 128;
Integer b = 128;
System.out.println(a == b); // false
System.out.println(a.equals(b)); // true

// 范围在 -128 ~ 127 内的数,用 == 比较为 true
Integer c = 127;
Integer d = 127;
System.out.println(c == d); // true

String e = "你好,世界";
String f = "你好,世界";
System.out.println(e == f); // true
System.out.println(e.equals(f)); // true

String g = new String("你好,世界");
String h = new String("你好,世界");
System.out.println(g == h); // false
System.out.println(g.equals(h)); // true

String s1 = "你好,世界";
String s2 = new String("你好,世界");
String s3 = new String("你好,世界");
System.out.println(s1 == s2); // false
System.out.println(s2 == s3); // false
}

双引号直接写的字符串是在常量池之中,而new的对象则不在池之中。

数据类型

包管理

修饰符 当前类 同一包内 同一包内的子类 不同包内的子类 其他包
public
protected 可访问父类的protected,不可访问父类对象的protected ×
default × ×
private × × × ×

instanceof:判断一个变量是否是某个类的实例

1
2
3
4
5
// Car 是 Vehicle 的子类
Car car = new Car();
boolean flag1 = car instanceof Car // flag1 = true
boolean flag2 = car instanceof Vehicle // flag2 = true
boolean flag3 = car instanceof Cat // flag3 = false

Math

原始值 floor(向下取整) round(四舍五入) ceil(向上取整)
2.7 2 3 3
2.3 2 2 3
-2.3 -3 -2 -2
-2.7 -3 -3 -2

floor是地板的意思,ceil是天花板,就很好记了

try & catch & finally

try中存在return的情况下,会把try中return的值存到栈帧的局部变量表中,然后去执行finally语句块,最后再从局部变量表中取回return的值返回。

当try和finally里都有return时,会忽略try的return,而使用finally的return。

正常情况下,finally中的代码一定会得到执行。但是如果我们将执行try-catch-finally代码块的线程设置为守护线程,或者在fianlly之前调用System.exit结束当前虚拟机,那么finally则不会得到执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 在fianlly之前调用 System.exit 结束当前虚拟机
try {
System.exit(0);
} catch (Exception e) {
// TODO
} finally {
// TODO
}

Thread thread = new Thread() {
@Override
public void run() {
// try-catch-finally
}
};

thread.setDaemon(true); // 将 thread 设为守护线程
thread.start();

throw & throws

throw用于主动抛出异常:

1
2
3
4
5
6
7
8
9
10
11
public double divide(double x, double y) {
try {
if (y == 0) {
throw new ArithmeticException();
} else {
return x / y;
}
} catch (ArithmeticException exception) {
exception.printStackTrace();
}
}

加了throws的函数,函数内部抛出的异常要在函数外捕捉到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 当抛出异常时,不在函数内捕捉,而是抛出到函数外
public double divide(double x, double y) throws ArithmeticException {
if (y == 0) {
throw new ArithmeticException();
}
return x / y;
}

// 捕捉到divide方法内部抛出的异常
public static void main(String[] args) {
try {
divide(5, 0);
} catch (ArithmeticException exception) {
exception.printStackTrace();
}
}

异常与错误

迭代器

1
2
3
4
5
6
HashSet<Integer> set = new HashSet<>();
Iterator<Integer> it = set.iterator();
while (it.hasNext()) {
int num = it.next();
//TODO
}

ArrayList 与 LinkedList 的区别

底层数据结构

  • ArrayList 使用动态数组来存储元素,这意味着在内存中分配一块连续的内存空间来保存元素

  • LinkedList 使用双向链表来存储元素,每个元素都包含对前一个和后一个元素的引用

插入和删除操作

  • ArrayList 的随机访问非常快速,因为可以通过索引直接访问元素。但是,插入和删除元素时,需要移动后续元素,效率较低

  • LinkedList 的插入和删除操作效率较高,因为只需更改节点的引用。但是,随机访问元素效率较低,因为必须从头或尾部开始遍历链表

内存消耗

  • ArrayList 在存储大量元素时可能会浪费一些内存,因为它分配一块较大的内存空间。但它在随机访问时效率较高

  • LinkedList 每个元素都需要额外的内存来存储引用,因此在存储大量元素时可能会消耗更多内存。但它在插入和删除操作时效率较高

适用场景

  • ArrayList 适用于需要频繁随机访问元素的情况,但不需要频繁执行插入和删除操作的情况

  • LinkedList 适用于需要频繁执行插入和删除操作的情况,但不需要频繁随机访问元素的情况

String相关

详见Java中的String

泛型

泛型的优点:

  • 类型安全:泛型在编译时提供了类型检查,可以在编译阶段捕获类型错误,而不是在运行时抛出异常
  • 更好的性能:泛型的类型检查是在编译时进行的,不需要运行时的类型检查,因此可以提高程序的性能
  • 代码复用:泛型可以编写更通用、可复用的代码,减少代码的冗余
  • 可读性:使用泛型可以在代码中看到操作的数据类型,提高代码的可读性

泛型擦除:泛型信息在编译后会被擦除,在运行时,所有的泛型类型参数都会被视为Object类型

线程

线程状态转换图

继承 Thread 类

自定义一个类继承Thread,重写其run方法,实例化后调用其start方法开启线程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/// MyThread.java
public class MyThread extends Thread {
private final String name;

MyThread(String name) {
this.name = name;
}

@Override
public void run() {
super.run();
System.out.println("我是线程" + name);
}
}

/// main
public static void main(String[] args) {
MyThread threadA = new MyThread("A");
MyThread threadB = new MyThread("B");
threadA.start();
threadB.start();
System.out.println("主线程结束");
}

运行结果可能是先A后B,也可能是先B后A。

实现 Runnable 接口

创建自定义类实现Runnable接口,重写其run方法。创建一个Thread对象,传入的参数即自定义类的一个实例,再调用Thread对象的start方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/// MyRunnable.java
public class MyRunnable implements Runnable {
final private String name;

MyRunnable(String name) {
this.name = name;
}

@Override
public void run() {
System.out.println("我是线程" + name);
}
}

/// main
public static void main(String[] args) {
MyRunnable runnableC = new MyRunnable("C");
MyRunnable runnableD = new MyRunnable("D");
Thread threadC = new Thread(runnableC);
Thread threadD = new Thread(runnableD);
threadC.start();
threadD.start();
System.out.println("主线程结束");
}

Callable

创建自定义类实现Callable接口,重写call方法,即子线程要实现的逻辑,且有一个泛型返回值。使用FutureTask类来包装Callable对象并启动线程,主线程将会等待子线程结束后,通过FutureTask对象的get方法获取其返回值,然后才会往下执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/// MyCallable.java
public class MyCallable<V> implements Callable<V> {
final private V data;

MyCallable(V data) {
this.data = data;
}

@Override
public V call() {
return data;
}
}

/// main
public static void main(String[] args) {
MyThread threadA = new MyThread("A");
threadA.start();

System.out.println("主线程开始实例化Callable");

MyCallable<String> myCallable = new MyCallable<>("E");
FutureTask<String> myFutureTask = new FutureTask<>(myCallable);
Thread threadE = new Thread(myFutureTask);
threadE.start();
try {
String data = myFutureTask.get();
System.out.println("我是线程" + data);
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}

System.out.println("主线程结束");
}

输出结果:我是线程E主线程开始实例化Callable之后,在主线程结束之前。

LocalDateTime & LocalDate & LocalTime

参考的是这篇文章

三者区别是,LocalDate只能存日期,LocalTime只能存时间,LocalDateTime既可以存日期又可以存时间。这里只介绍LocalDateTime。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 实例化
var localDateTime1 = LocalDateTime.now();
var localDateTime2 = LocalDateTime.of(2023, 9, 11, 22, 23, 00); // 2023-09-11 22:23:00

// 字符串转LocalDateTime
var localDateTime3 = LocalDateTime.parse("2023-09-11T22:23:00"); // 必须要有T
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
var localDateTime4 = LocalDateTime.parse("2023-09-11 22:23:00", formatter); // 将字符串按照formatter中规定的格式转化

// 获取星期
DayOfWeek dayOfWeek = localDateTime1.getDayOfWeek(); // 假设为星期天
System.out.println(dayOfWeek.getValue()); // 7
// TextStyle.FULL(星期全称), Locale.CHINA(用中文输出)
System.out.println(dayOfWeek.getDisplayName(TextStyle.FULL, Locale.CHINA)); // 星期日
// TextStyle.FULL(星期全称), Locale.CHINA(用中文输出)
System.out.println(dayOfWeek.getDisplayName(TextStyle.Short, Locale.CHINA)); // 周日

// 获取月份同上

// 增加日期
localDateTime1 = localDateTime1.plus(5, ChronoUnit.DAYS); // 日期+5,下同
localDateTime1 = localDateTime1.plusDays(5);

Date & Calendar (已弃用)

看完一问chatGPT,才知道这俩已经被LocalDateTime取代了,旧教程害人啊……笔记都做完了,就不删了吧

按自定义格式输出时间:

1
2
3
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss E");
System.out.println(format.format(date));
// 2023-09-09 19:54:33 周六

其中HH:mm:ss是24小时制,hh:mm:ss是12小时制

或者用printf:

1
2
System.out.printf("现在是:%tY-%tm-%td,%tp%tH:%tM:%tS,%tA", date, date, date, date, date, date, date, date);
// 现在是:2023-09-09,下午20:07:02,星期六

使用Calendar

1
2
3
4
5
Calendar calendar = Calendar.getInstance();     // 创建对象,默认为当前时间
calendar.set(2002, 11, 30); // 设置年月日
calendar.set(Calendar.YEAR, 2077); // 单独设置某个属性
System.out.println(calendar.get(Calendar.YEAR));
// 2077

有关Date类与Calendar类的更多信息,参考这里

注解

Java注解入门到精通,这一篇就够了