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注解入门到精通,这一篇就够了

如果没有对注解进行处理,注解和注释一样不会影响代码运行。

注解能让编译器在编译时动态处理。

元数据:接口、类、属性、方法。

元注解:@Retention@Target@Documented@Inherited@Repeatable

我们要自定义一个注解,按如下格式:

1
2
3
4
5
6
7
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Inherited
@Documented
public @interface MyAnnotation {
public int myMethod() default 0;
}

其实注解就是一个接口,所以内部的方法即使不加public也是public的。在程序运行时,JVM会为注解其生成对应的代理类。注解内部的方法就是个带默认返回值的接口方法。

注解是不能继承也不能实现其他类或接口的。

@Retention

标明自定义注解的生命周期。从编写Java代码到运行主要周期为源文件 → Class文件 → 运行时数据@Retention则标注了自定义注解的信息要保留到哪个阶段,分别对应的value取值为SOURCE → CLASS → RUNTIME

  • SOURCE:源代码java文件,生成的class文件中就没有该信息了。例如自定义一个注解@ThreadSafe,用来标识一个类是线程安全的,就和注释的作用一样,不过更引人注目罢了
  • CLASS:class文件中会保留注解,但是jvm加载运行时就没有了。例如标记一个@Proxy,JVM加载时就会生成对应的代理类
  • RUNTIME:运行时,如果想使用反射获取注解信息,则需要使用RUNTIME,反射是在运行阶段执行的,那么只有Runtime的生命周期才会保留到运行阶段,才能被反射读取,也是我们最常用的

@Target

描述自定义注解的使用范围,允许自定义注解标注在哪些Java元素上(类、方法、属性、局部属性、参数等),括号内的value可以是多个值。value允许的取值如下:

说明
TYPE 类、接口、注解、枚举
FIELD 属性
MEHOD 方法
PARAMETER 方法参数
CONSTRUCTOR 构造函数
LOCAL_VARIABLE 局部变量(如循环变量、catch参数)
ANNOTATION_TYPE 注解
PACKAGE
TYPE_PARAMETER 泛型参数 JDK1.8
TYPE_USE 任何元素 JDK1.8

@Inherited

@Inherited修饰的注解是具有继承性的,在自定义的注解标注到某个类时,该类的子类会继承这个自定义注解。

这里需要注意的是只有当子类继承父类的时候,注解才会被继承,类实现接口,或者接口继承接口,都是无法获得父接口上的注解声明的。

@Repeatable

是否可以重复标注。

@Repeatable

@Documented

是否在生成的JavaDoc文档中体现,被标注该注解后,生成的JavaDoc中,会包含该注解。

注解的工作原理

给类、方法、属性加上注解,就相当于加了个标记,在运行时通过反射获取到注解,继而执行需要处理的逻辑。

反射

Java反射学习总结

Class类的实例是由JVM进行创建的,无法手动创建,只能进行获取,通过不同方式获取的Class实例都是同一个实例。

获取Class对象的方式:

1
2
3
4
5
6
7
8
9
// 通过实例获取
Person saoke = new Animal();
Class<? extends Person> clazz1 = saoke.getClass();

// 通过类获取
Class<Person> clazz2 = Person.class;

// 通过全路径获取,无需任何依赖,只传入包路径+类名即可
Class<?> clazz3 = Class.forName("com.saoke.Person");

可以通过Class实例获取到对应类的构造方法、方法、属性,并进行调用。

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
35
36
37
38
39
40
41
42
43
44
45
46
class Person {
private String name;
private int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}

public String say(String words) {
//TODO
}
}

class Main {
public static void main(String[] args) {
Person saoke = new Person("骚客", 18);
Class<Person> clazz = saoke.getClass();

// 获取所有public的构造函数、方法、属性
Constructor[] constructors = clazz.getConstructors();
Method[] methods = clazz.getMethods();
Field[] fields = clazz.getFields();

// 获取所有的构造函数、方法、属性(public/protected/default/private)
constructors = clazz.getDeclaredConstructors();
methods = clazz.getDeclaredMethods();
fields = clazz.getDeclaredFields();

// 获取某个构造函数、方法、属性(非公开的同理用Declared)
Constructor constructor = clazz.getConstructor(String.class, int.class);
Method method = clazz.getMethod("say", String.class);
Field field = clazz.getDeclaredField("name");

// 若非public要先setAccessible
field.setAccessible(true);

// 调用构造函数
Person prince = constructor.newInstance("Prince", 25);
// 第一个参数是要执行该方法的对象,若该方法是static的,第一个参数为null
String response = method.invoke(saoke, "Java反射,轻而易举");
// get/set属性值
int age = field.get(saoke);
field.set(prince, 18);
}
}