Java是一门面向对象的编程语言,不仅吸收了C++语言的各种优点,还摒弃了C++里难以理解的多继承指针等概念,因此Java语言具有功能强大和简单易用两个特征。Java语言作为静态面向对象编程语言的代表,极好地实现了面向对象理论,允许程序员以优雅的思维方式进行复杂的编程。Java具有简单性、面向对象、分布式健壮性安全性、平台独立与可移植性、多线程、动态性等特点。 Java可以编写桌面应用程序、Web应用程序、分布式系统嵌入式系统应用程序等。

原文链接:全网最完整Java学习笔记

String类

基本介绍

String是一个类,用于存储字符串,内部封装了一系列用于操作字符串的方法,底层是final修饰的char数组。

JDK9开始,为了节省内存,进而减少垃圾回收次数,String底层由char数组改成了byte[]

Java的String和c++的string区别:

  • java中字符串一个汉字长度是1;
  • c++中字符串,一个汉字长度是2.

基本用法

创建

创建字符串有两种方式,一种是使用字符串直接量,另一种是使用new+构造器。采用new的方式会多创建出一个对象来,占用了更多的内存 ,所以建议采用直接量的方式来创建字符串。

字符串直接量创建:JVM会使用常量池来管理这个字符串;
new创建:JVM会先使用常量池来管理字符串直接量(若已有此字符串则直接返回引用,若没有则实例化后再返回引用),再调用String类的构造器来创建一个新的String对象,新创建的String对象会被保存在堆内存中。字符串常量池源码用到了享元设计模式。

1
2
3
4
5
6
// String 直接创建
String s1 = "Runoob";
// String 对象创建
String s2 = new String("Runoob");
// 引用赋值的方法创建字符串
String s3 = s1;

访问

自带方法访问

1
2
3
4
5
6
7
8
9
// 通过索引访问字符:charAt()
String str = "Hello, World!";
char firstChar = str.charAt(0);
System.out.println("First Character: " + firstChar);
// 访问子串:substring()
String str = "0123456789ABCDEFG";
// 提取索引 7 到 11 的子串
String substring = str.substring(7, 12);
System.out.println("Substring: " + substring);

遍历字符串每个字母

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 使用for循环遍历字符串
String str = "hello world";
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
System.out.println(c);
}
// 使用增强for循环遍历字符串
for (char c : str.toCharArray()) {
System.out.println(c);
}
// 使用while循环遍历字符串
int i = 0;
while (i < str.length()) {
char c = str.charAt(i);
System.out.println(c);
i++;
}
// 使用Iterator遍历字符串
Iterator<Character> iterator = str.chars().mapToObj(c -> (char) c).iterator();
while (iterator.hasNext()) {
char c = iterator.next();
System.out.println(c);
}

连接

字符串可以通过“+”号拼接,拼接过程中可以将数字型转为字符串。字符串拼接数字,数字会转成字符串。

字符串连接字符数组

1
2
3
4
5
6
7
8
9
10
// 连接字符串
String s = "Hello";
s = s + ", World!";
// 连接字符数组
char[] greeting = {'H', 'e', 'l', 'l', 'o'};
char[] suffix = {',', ' ', 'W', 'o', 'r', 'l', 'd', '!'};
char[] result = new char[greeting.length + suffix.length];
System.arraycopy(greeting, 0, result, 0, greeting.length);
System.arraycopy(suffix, 0, result, greeting.length, suffix.length);
System.out.println(result);

子串

在 Java 中,可以使用 substring() 方法获取字符串的子串:

1
2
3
4
5
6
7
8
// substring(int beginIndex): 返回从指定索引开始到字符串末尾的子串。
String originalString = "Hello, World!";
String substring = originalString.substring(7); // 从索引 7 开始到字符串末尾的子串
System.out.println(substring); // 输出结果为 "World!"
// substring(int beginIndex, int endIndex): 返回从指定索引开始到指定索引结束的子串(不包括 endIndex 处的字符)。
String originalString = "Hello, World!";
String substring = originalString.substring(7, 12); // 从索引 7 开始到索引 12 结束的子串
System.out.println(substring); // 输出结果为 "World"

注意:

因为String是不可变的,对字符串的任何操作都会返回一个新的字符串。所以substring 方法其实是创建了一个字符子串,而不是修改了原始字符串。

删除

Java中字符串没有直接根据索引删除的方法,所以删除字符串中指定索引的字母时,可以通过子串substring()删除。

示例:删除字符串中下标是6的字母

1
2
3
String str = "Hello, World!";
String newStr = str.substring(0, 5) + str.substring(7);
System.out.println("Modified String: " + newStr);

替换

在Java中, 可以使用replaceAll()方法替换字符中的字符:

1
2
3
4
5
6
7
8
9
// replaceAll(String regex, String replacement):使用给定的替换字符串替换输入字符串中所有匹配正则表达式的部分。
String originalString = "Hello, World!";
String replacedString = originalString.replaceAll("o", "0");
System.out.println(replacedString); // 输出结果为 "Hell0, W0rld!"

// replaceAll(String regex, Function<MatchResult, String> replacer):使用给定的 Function 替换输入字符串中所有匹配正则表达式的部分。该方法允许更加灵活的替换逻辑,并且可以基于匹配的结果进行自定义的替换。
String originalString = "Hello, World!";
String replacedString = originalString.replaceAll("o", match -> match.group().toUpperCase());
System.out.println(replacedString); // 输出结果为 "HellO, WOrld!"

获取长度

可以通过length()方法获取字符串的长度。

1
2
3
4
5
6
7
8
public class Test {
public static void main(String[] args) {
String text = "Java Programming";
// 返回字符串的长度
int length = text.length();
System.out.println(length);
}
}

比较

字符串内容的互相比较一般用equals()方法。Java中所有类都直接或间接继承了 Object 类,在 Object 类中,equals() 方法是 Java 中用于比较两个对象的引用是否相等(即内存地址是否相同)。

在 String 类中,equals() 方法被重写,用于比较两个字符串的内容是否相等。

注意:尽量不要用==进行比较,==比较的是地址。

String底层是常量池,使用享元设计模式,新创建的字符串会维护在常量池,下次再创建这个字符串,就直接从常量池取。

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
public class Test {
public static void main(String[] args) {
// 1.比较地址
// 只要new,就在堆内存开辟空间。直接赋值字符串在常量池里。
// 常量池里无“hello”对象,创建“hello”对象,str1指向常量池“hello”对象。
String str1 = "hello";
// 先检查字符串常量池中有没有"hello"
// 如果字符串常量池中没有,则创建一个,然后 str1 指向字符串常量池中的对象,
// 如果有,则直接将 str1 指向"hello";
// 常量池里有“hello”对象,str2直接指向常量池“hello”对象。
String str2 = "hello";
// 堆中new创建了一个对象。假如“hello”在常量池中不存在,Jvm还会常量池中创建这个对象“hello”。
String str3 = new String("hello");
String str4 = new String("hello");
// 下面输出true,因为str1和str2指向的是常量池中的同一个内存地址
System.out.println(str1 == str2);
// 下面输出false,str1常量池旧地址,str3是new出的新对象,指向一个全新的地址
System.out.println(str1 == str3);
// 下面输出false,因为它们引用不同
System.out.println(str4 == str3);

// 2.比较内容
// 下面输出true,因为String类的equals方法重写过,比较的是字符串值
System.out.println(str4.equals(str3));
}
}

==与equals()的区别

== 比较基本数据类型时,比较的是两个数值是否相等; 比较引用类型是,比较的是对象的内存地址是否相等。
equals() 没有重写时,Object默认以==来实现,即比较两个对象的内存地址是否相等; 重写以后,按照重写的逻辑进行比较。
示例:String源码中重写的equals()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// public final class String
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
return (anObject instanceof String aString)
&& (!COMPACT_STRINGS || this.coder == aString.coder)
&& StringLatin1.equals(value, aString.value);
}

// final class stringLatin1
@IntrinsicCandidate
public static boolean equals(byte[] value, byte[] other) {
if (value.length == other.length) {
for (int i = 0; i < value.length; i++) {
if (value[i] != other[i]) {
return false;
}
}
return true;
}
return false;
}
  • 如果地址一样,则一定相等;
  • 如果对比的元素不是String类型,则一定不相等;
  • 遍历String底层的数组,逐个对比字符是否相等;

分割

split() 方法是 Java 中 String 类的一个方法,用于将字符串分割成字符串数组,根据给定的正则表达式作为分隔符。该方法有两个重载的版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 1.split(String regex): 使用给定的正则表达式作为分隔符,将字符串分割为字符串数组。
String sentence = "Hello, World! How are you?";
String[] words = sentence.split(" "); // 使用空格作为分隔符
for (String word : words) {
System.out.println(word);
}

// 2.split(String regex, int limit): 使用给定的正则表达式作为分隔符,将字符串分割为字符串数组,限制分割的次数。
String sentence = "apple,orange,banana,grape";
String[] fruits = sentence.split(",", 2); // 使用逗号作为分隔符,限制分割次数为 2
for (String fruit : fruits) {
System.out.println(fruit);
}
// 输出
// apple
// orange,banana,grape

// 注意:split() 方法的参数是正则表达式,因此在传入正则表达式时,可能需要注意转义字符的使用。例如,如果要以点号 . 作为分隔符,由于点号在正则表达式中有特殊含义,需要使用双反斜杠 \ 进行转义。
String sentence = "one.two.three";
String[] parts = sentence.split("\\."); // 使用点号作为分隔符,需要转义
for (String part : parts) {
System.out.println(part);
}

转换大小写

1
2
3
String text = "Java Programming";
String lowerCase = text.toLowerCase();
String upperCase = text.toUpperCase();

字符串和数字的互相转换

1. 字符串转整数:

1
2
3
4
5
6
7
String strNumber = "123";
// 使用 Integer.parseInt() 方法
int intValue = Integer.parseInt(strNumber);
System.out.println("Parsed Integer: " + intValue);
// 使用 Integer.valueOf() 方法
Integer integerValue = Integer.valueOf(strNumber);
System.out.println("Integer Value: " + integerValue);

2. 整数转字符串:

1
2
3
4
5
6
7
int intValue = 123;
// 使用 String.valueOf() 方法
String strNumber = String.valueOf(intValue);
System.out.println("String Value: " + strNumber);
// 使用 Integer.toString() 方法
String strNumber = Integer.toString(intValue);
System.out.println("String Value: " + strNumber);

数组和字符串的互相转换

  1. 数组转List:List arrayToList(T[] array)
  2. List转数组:T[] listToArray(List list, Class elementType)
  3. 数组转Set:Set arrayToSet(T[] array)
  4. Set转数组:T[] setToArray(Set set, Class elementType)
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
// 1. 数组转List
String[] array = {"apple", "banana", "orange"};
List<String> listFromArray = Arrays.asList(array);
System.out.println("List from Array: " + listFromArray);
// 2. List转数组
List<String> fruitList = new ArrayList<>(List.of("apple", "banana", "orange"));
String[] arrayFromList = fruitList.toArray(new String[0]);
System.out.println("Array from List: " + Arrays.toString(arrayFromList));
// 3. List转Set
Set<String> setFromList = new HashSet<>(fruitList);
System.out.println("Set from List: " + setFromList);
// 4. Set转List
Set<String> fruitSet = new HashSet<>(Set.of("apple", "banana", "orange"));
List<String> listFromSet = new ArrayList<>(fruitSet);
System.out.println("List from Set: " + listFromSet);
// 5. Set转数组
String[] arrayFromSet = fruitSet.toArray(new String[0]);
System.out.println("Array from Set: " + Arrays.toString(arrayFromSet));
// 6. 数组转Set
Set<String> setFromArray = new HashSet<>(Arrays.asList(array));
System.out.println("Set from Array: " + setFromArray);

// 转为集合后,可以通过集合的api操作各元素。例如:判断数组是否包含某个元素:
int[] array = {1, 2, 3, 4, 5};
boolean isEle = Arrays.asList(array).contains("a");
System.out.println(isEle);

格式化数字为字符串

1
2
3
4
double number = 123.456789;
// 格式化浮点数为字符串,保留两位小数
String formattedString = String.format("%.2f", number);
System.out.println(formattedString);

知识加油站

字符串和字符数组的区别

  • 可变性:字符串是不可变的。一旦创建,字符串的内容就不能被修改。任何对字符串的修改底层都会创建一个新的字符串对象。而字符数组是可变的。你可以直接修改字符数组的元素。
  • 方法数量:String类提供了许多用于处理字符串的方法,如拼接、比较、截取、转换大小写等。而字符数组只有toString()、equals()等简单的数组通用方法。
  • 性质:String是类(底层是字节数组),字符数组是数组。
  • 创建方式:String str = “Hello”;char[] charArray = {‘H’, ‘e’, ‘l’, ‘l’, ‘o’};
  • 连接:字符串通过“+”连接,字符数组通过System.arraycopy()连接。
  • 使用场景:String不可变,所以适用于不需要频繁修改字符串内容的情况。字符数组适用于需要频繁修改字符内容的情况

字符串常量池

常量池:Java虚拟机有一个常量池机制,它会直接把字符串常量放入常量池中,从而实现复用。

Java字符串存储原理

创建字符串常量时,JVM会通过equals()检查字符串常量池中是否存在这个字符串;
若字符串常量池中存在该字符串,则直接返回引用实例;
若不存在,先实例化该字符串,并且将该字符串的引用放入字符串常量池中,以便于下次使用时,直接取用,达到缓存快速使用的效果。

String不可被继承、不可变的原因

String为什么不可被继承?

因为String类底层的数组是由final修饰的,所以String类不可被继承。

String字符串为什么不可被变?

因为String底层char类型的value数组是private final修饰的。

  • final修饰:导致value不能指向新数组(但无法保证value这个引用变量指向的真实数组不可变);
  • private修饰,且没对外暴露任何修改value的方法:导致value这个引用变量指向的底层数组不可变;

不可变的优点:因为压根不会被改,所以线程安全、节省空间、效率高。

new String(“abc”)创建的字符串对象数量

一个或两个。原因如下

  • 首先,new string 这边由于 new 关键字,所以这边肯定会在堆中直接创建一个字符串对象。

  • 其次,如果字符串常量池中不存在 “abc”(通过equals比较)这个字符串的引用,则会在字符串常量池中创建一个字符串对象。如果已存在则不创建。注意这边说的在字符串常量池创建对象,最终对象还是在堆中创建,字符串常量池只放引用。


StringBuilder 类

基本介绍

String拼接字符串后原字符串还存在于内存中,浪费内存。

StringBuffer 类的对象能够被多次的修改,并且不产生新的未使用对象,所以涉及到字符串拼接,优先用StringBuffer。

1
2
3
4
5
6
7
8
// 构造
StringBuilder sb = new StringBuilder("abc");
// String转StringBuilder
sb.append("d").append("e").append("f");
// StringBuilder转String
String s = sb.toString();
// 反转
sb.reverse();

String、StringBuffer、Stringbuilder的区别

String:不可变字符序列,效率低,但是复用率高、线程安全。

不可变是指String对象创建之后,直到这个对象销毁为止,对象中的字符序列都不能被改变。

复用率高是指String类型对象创建出来后归常量池管,可以随时从常量池调用同一个String对象。StringBufferStringBuider在创建对象后一般要转化成String对象才调用。

StringBufferStringBuilder都是字符序列可变的字符串,方法也一样,有共同的父类AbstractStringBuilder

  • StringBuffer:可变字符序列、效率较高(增删)、线程安全
  • StringBuilder:可变字符序列、效率最高、线程不安全

Scanner类

该类用于从各种输入源(如控制台)中获取基本数据类型和字符串,如 int、double、String 等。常用于从控制台、文件等读取数据。

构造方法

  • Scanner(InputStream source): 构造一个新的 Scanner,生成的扫描器从指定的输入流读取数据。一般用System.in,即标准输入流,用于读取用户在控制台输入的数据。
  • Scanner(File source): 构造一个新的 Scanner,生成的扫描器从指定的文件读取数据。
  • Scanner(String source):构造一个新的 Scanner,生成的扫描器从指定的字符串读取数据。

常用方法

  • nextInt()、nextDouble()、next():获取输入的整数、浮点数、字符串(不包括空格)等。
  • nextLine():获取一行输入(包括空格)。
  • hasNextInt()、hasNextDouble()、hasNext(): 判断下一个输入是否为整数、浮点数、字符串。
  • useDelimiter(String pattern): 设置分隔符模式,用于指定不同类型数据之间的分隔符,默认为空白字符。
  • close():关闭扫描器。

示例:从控制台输入整数、浮点数、字符串,并在控制台打印。

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
import java.util.Scanner;

public class Test {
public static void main(String[] args) {
// 创建 Scanner 对象,关联 System.in(标准输入流)
Scanner scanner = new Scanner(System.in);
// 从控制台读取整数
System.out.print("Enter an integer: ");
int intValue = scanner.nextInt();
System.out.println("You entered: " + intValue);
// 从控制台读取浮点数
System.out.print("Enter a double: ");
double doubleValue = scanner.nextDouble();
System.out.println("You entered: " + doubleValue);
// 从控制台读取字符串
System.out.print("Enter a string: ");
String stringValue = scanner.next();
System.out.println("You entered: " + stringValue);
// 关闭 Scanner 对象
scanner.close();
}
}
// 输出结果
// Enter an integer: 1
// You entered: 1
// Enter a double: 2
// You entered: 2.0
// Enter a string: 4
// You entered: 4

Object类

基本介绍

在 Java 中,Object 类是所有类的根类,所有其他类都直接或间接地继承自 Object 类。

Object 类定义了一些基本的方法,这些方法可以被所有对象继承和使用:

  1. toString() 方法: 返回对象的字符串表示。再不重写的情况下,toString() 返回的是对象的类名和散列码的十六进制表示。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class MyClass {
    private int value;
    public MyClass(int value) {
    this.value = value;
    }
    // 自定义转字符串逻辑
    @Override
    public String toString() {
    return "MyClass{" +
    "value=" + value +
    '}';
    }
    }

    // 使用 toString() 方法
    MyClass obj = new MyClass(42);
    // toString将根据自己重新的逻辑返回字符串,而不是返回对象的类名和散列码的十六进制表示
    System.out.println(obj.toString()); // 输出结果为 "MyClass{value=42}"
  2. equals(Object obj) 方法: 比较对象是否相等。默认情况下,equals() 方法比较的是对象的引用(即内存地址),重写后可以自定义比较逻辑。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class Person {
    private String name;
    private int age;
    // ... 其他代码 ...
    // 自定义比较逻辑
    @Override
    public boolean equals(Object obj) {
    if (this == obj) return true;
    if (obj == null || getClass() != obj.getClass()) return false;
    Person person = (Person) obj;
    return age == person.age && Objects.equals(name, person.name);
    }
    }
  1. hashCode() 方法: 返回对象的散列码。hashCode() 方法的默认实现返回对象的内存地址的散列码。通常情况下,如果 equals() 方法被覆盖,那么也应该同时覆盖 hashCode() 方法。

    1
    2
    3
    4
    @Override
    public int hashCode() {
    return Objects.hash(name, age);
    }
  2. getClass() 方法: 返回对象的类。该方法基于反射,返回类的 Class 对象,该对象包含有关对象的类的信息。

    1
    2
    3
    MyClass obj = new MyClass(42);
    Class<?> clazz = obj.getClass();
    System.out.println(clazz.getName()); // 输出结果为 "MyClass"
  3. clone() 方法: 创建并返回对象的拷贝。要实现 clone() 方法,类必须实现 Cloneable 接口。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class CloneableClass implements Cloneable {
    private int value;
    public CloneableClass(int value) {
    this.value = value;
    }
    @Override
    protected Object clone() throws CloneNotSupportedException {
    return super.clone();
    }
    }
  4. finalize() 方法: 在对象被垃圾收集器回收之前调用。通常不推荐使用 finalize() 方法,因为它的行为是不确定的,并且可能导致一些问题。

    1
    2
    3
    4
    5
    6
    @Override
    protected void finalize() throws Throwable {
    // 执行清理工作
    // ...
    super.finalize();
    }

JVM垃圾回收的可达性分析算法

JVM垃圾回收的可达性分析算法有用到Object类的finalize() 方法。

可达性分析算法

以根对象集合(GC Roots)的每个跟对象为起始点,根据引用关系向下搜索,将所有与GC Roots直接或间接有引用关系的对象在对象头的Mark Word里标记为可达对象,即不需要回收的有引用关系对象。搜索过程所走过的路径称为“引用链” 。

**GC Roots**:即GC根节点集合,是一组必须活跃的引用。可作为GC Roots的对象:

  • 栈引用的对象:Java方法栈、本地方法栈中的参数引用、局部变量引用、临时变量引用等。临时变量是方法里的中间操作结果。
  • 方法区中常量、静态变量引用的对象;
  • 所有被同步锁持有的对象;
  • 所有线程对象;
  • 所有跨代引用对象;
  • JVM内部的引用:如基本数据类型对应的Class对象,常驻的异常对象,以及应用程序类类加载器;

非可达对象被回收需要两次标记

  1. 第一次标记后筛选非可达对象:第一次被标记后,会进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法,也就是是否有机会自救。假如对象没有覆盖或者已被JVM调用过finalize()方法,也就是说不想自救或已自救过,那么此对象需要被回收;假如对象覆盖并没被JVM调用过finalize()方法,该对象将会被放置在一个名为F-Queue的队列之中,并在稍后由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize()方法。
  2. 第二次标记F-Queue里的未自救对象:稍后,收集器将对F-Queue中的对象进行第二次小规模的标记。如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this)赋值给某个引用类型的类变量或者对象的成员变量,那在第二次标记时它将被移出“即将回收”的F-Queue。如果对象这时候还没有逃脱,那基本上它就真的要被回收了。

finalize()方法

finalize()方法是对象逃脱死亡命运的最后一次机会,需要注意的是,任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行。

另外,finalize()方法的运行代价高昂,不确定性大,无法保证各个对象的调用顺序,如今已被官方明确声明为不推荐使用的语法。

hashCode()和equals()的区别

用途:

  • hashCode()方法的主要用途是获取哈希码;
  • equals()主要用来比较两个对象是否相等。

为什么重写equals()就要重写hashcode()?

因为二者之间有两个约定,相等对象的哈希码也要相等。

所以equals()方法重写时,通常也要将hashCode()进行重写,使得这两个方法始终满足相关的约定。 例如HashSet排序机制底层就是通过计算哈希码进行排序的,如果只重写equals()将达不到根据哈希码排序的效果。

如果两个对象相等,它们必须有相同的哈希码;但如果两个对象的哈希码相同,他们却不一定相等。

哈希碰撞:由于哈希码是一个有限的整数,因此有可能会出现不同的对象计算出相同的哈希码的情况。 例如某类里有两个int型成员变量,重写后equals()比较两个变量是否相等,hashcode()是两个变量的和,A对象变量是2和3,B对象变量是1和4,加起来都是5,它们哈希码相等而不equal。


System类

System 类是 Java 标准库中的一个工具类,提供了与系统相关的一些操作。

它包含一些静态方法和字段,用于访问系统属性、标准输入输出、垃圾回收等。

常用方法和字段:

PrintStream、PrintStream等IO流详细看后文的I/O流。

  1. out 字段: 是 PrintStream 类的一个实例,用于标准输出。可以使用 System.out.println() 来向标准输出打印信息。

    1
    System.out.println("Hello, World!");  // 将 "Hello, World!" 输出到标准输出
  2. err 字段: 是 PrintStream 类的一个实例,用于标准错误输出。可以使用 System.err.println() 来向标准错误输出打印错误信息。

    1
    System.err.println("This is an error message.");  // 将错误信息输出到标准错误输出
  3. in 字段: 是 InputStream 类的一个实例,用于标准输入。可用 Scanner 或 BufferedReader 等类从标准输入读取用户输入信息。

    1
    2
    3
    Scanner scanner = new Scanner(System.in);
    System.out.print("Enter your name: ");
    String name = scanner.nextLine();
  4. currentTimeMillis() 方法: 返回当前时间与1970年1月1日午夜之间的毫秒数。通常用于测量代码的执行时间。

    1
    2
    3
    4
    long startTime = System.currentTimeMillis();
    // 执行一些操作
    long endTime = System.currentTimeMillis();
    long elapsedTime = endTime - startTime;
  5. arraycopy(Object src, int srcPos, Object dest, int destPos, int length) 方法: 用于复制数组。

    1
    2
    3
    4
    int[] sourceArray = {1, 2, 3, 4, 5};
    int[] destinationArray = new int[5];
    System.arraycopy(sourceArray, 0, destinationArray, 0, sourceArray.length);
    // 现在 destinationArray 中包含了 sourceArray 的内容
  6. getProperty(String key) 方法: 获取系统属性。可以用于获取一些系统相关的信息,比如操作系统类型、Java 版本等。

    1
    2
    String osName = System.getProperty("os.name");
    System.out.println("Operating System: " + osName);
  7. exit(int status) 方法: 终止当前运行的 Java 虚拟机。status 参数是一个整数,通常用于指示程序的退出状态。非零值通常表示发生了错误。

    1
    2
    System.exit(0);  // 正常退出程序
    System.exit(1); // 异常退出程序
  8. gc() 方法: 强制调用垃圾回收器。虽然 Java 具有自动垃圾回收机制,但可以使用 System.gc() 来显式地触发垃圾回收。

    1
    System.gc();  // 强制调用垃圾回收器

Integer类

基本介绍

Integer类是 Java 中用于表示整数的包装类,它提供了许多方法来对整数进行操作和转换。

相比于基本数据类型,包装类的优势:可以有更多的方法操作改数据。

1
2
3
4
5
6
7
8
9
10
11
// 装箱:基本数据类型转为包装类
Integer a = Integer.valueOf(32);
Integer a2 = 32;
// 拆箱:包装类转为基本数据类型
int b = a.intValue();
// 数字转String
String s = String.valueOf(12);
// String转数字
int c = Integer.parseInt(s);
// String转Integer转数字
int c = Integer.valueOf(s).intValue();

包装类的自动拆装箱与自动装箱

包装类:包装类的主要作用是用于便于操作基本数据类型,将基本数据类型转换为对象,让基本数据类型拥有对象的特征,例如封装方法、泛型(基本数据类型不能作为泛型参数)、反射。

自动装箱是指把一个基本类型的数据直接赋值给对应的包装类型;

自动拆箱是指把一个包装类型的对象直接赋值给对应的基本类型;

向上转型:布尔型外的基本数据类型在互相比较或运算时会向上转型:byte,short,char → int → long → float → double。原理是将低字节数的数据类型转换为高字节数的数据类型,可以保证数据精度不丢失。c语言转型顺序:char→short→int→long→float→double

什么情况下用包装类?什么情况下用基本数据类型?

包装类适用场景

  • 实体类属性必须使用包装类:《阿里规约》规定,所有的 POJO 类属性必须使用包装数据类型,而不是基本数据类型。因为数据库的查询结果可能是 null,因为自动拆箱,用基本数据类型接收有 NPE 风险(NullPointerException空指针异常)。
  • RPC方法的返回值和参数必须使用包装类:《阿里规约》规定,RPC 方法的返回值和参数必须使用包装数据类型。因为相比基本数据类型,包装类的null值能展示额外的信息。例如远程调用获取商品价格,如果用包装类,null表示获取失败,0表示价格是0;而如果用基本数据类型,即使获取失败返回值也是0,你就没法知道是价格0还是获取失败了。

基本数据类型适用场景

  • 局部变量尽量使用基本数据类型:《阿里规约》建议,所有的局部变量使用基本数据类型。因为包装类对象是引用类型,JVM中,基本数据类型存储在方法栈中,引用数据类型存储堆内存实际对象的地址值,如果局部变量定义为引用数据类型还得根据这个地址值去找值,性能差(每次要new),而且耗费空间,毕竟它的作用域只是方法内。

包装类和基本数据类型直接如何互相比较?(浮点数等号比较精度丢失问题)

包装类和基本数据类型:直接通过==比较。

1
2
3
4
Integer integer = new Integer(3000);
double b = 3000;
// true,相等
System.out.println(b == integer);

整型

  • 相同整型包装类必须通过equals()比较。虽然两个通过自动装箱创建的、数值在缓存范围的相同整型包装类可以通过==比较(例如Integer a=1,b=1,则a==b),但《阿里规约》规定整型包装类必须通过equals()比较。包装类和各类型基本类型可以通过==比较。
  • 不同整型包装类必须转成相同类再通过equals()比较。
  • 整型基本数据类型用==比较。

浮点型

  • 浮点包装类先都转为BigDecimal,再进行运算、比较。

  • 浮点基本类型直接比较,要声明误差,两浮点数差值在此范围内,认为是相等的。

浮点数正确比较方法

基本数据类型

1
2
3
4
5
6
float a = 1.0f - 0.9f;
float b = 0.9f - 0.8f;
float diff = 1e-6f;
if (Math.abs(a - b) < diff) {
System.out.println("a、b相等");
}

包装类:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 为了保证高精度
// BigDecimal推荐入参为 String 的构造方法,或使用 BigDecimal 的 valueOf 方法
// valueOf方法内部其实执行了Double 的 toString,而 Double 的 toString 按 double 的实际能表达的精度对尾数进行了截断
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = BigDecimal.valueOf(0.9);
BigDecimal c = new BigDecimal("0.8");

BigDecimal x = a.subtract(b);
BigDecimal y = b.subtract(c);

if (x.equals(y)) {
System.out.println("a、b相等");
}

Integer a1=127;Integer a2=127;a1==a2原因

享元模式

Integer 内部有享元模式设计,【-128,127】范围内的数字会被缓存,使用自动装箱方式赋值时,Java默认通过valueOf()方法对127这个数字进行装箱操作,触发缓存机制,使a1和a2指向同一个内存地址。

  • Byte、Short、Integer、Long 缓存区间【-128,127】。
  • Character 包装类型缓存区间[0,127]。
  • 浮点型和布尔型没用享元模式,没有缓存区间。

Arrays类

Arrays 类包含了一系列用于操作数组的静态方法。可以对数组排序、搜索、比较、转换、填充等。以下是 Arrays 类的一些常用方法:

排序

1
2
3
4
5
6
7
// sort(T[] a):对数组进行升序排序
int[] numbers = {5, 2, 8, 1, 6};
Arrays.sort(numbers);

// sort(T[] a, Comparator<? super T> c): 使用指定的比较器对数组进行排序
String[] names = {"John", "Alice", "Bob", "Eve"};
Arrays.sort(names, Comparator.reverseOrder());

查找和判断

1
2
3
4
5
6
7
8
// binarySearch(T[] a, T key): 在已排序的数组中使用二分查找算法查找指定元素的索引
int[] numbers = {1, 2, 3, 4, 5, 6};
int index = Arrays.binarySearch(numbers, 3);

// equals(T[] a, T[] a2): 比较两个数组是否相等
int[] arr1 = {1, 2, 3};
int[] arr2 = {1, 2, 3};
boolean isEqual = Arrays.equals(arr1, arr2);

批量填充

1
2
3
// fill(T[] a, T val): 使用指定的值填充数组的所有元素
int[] numbers = new int[5];
Arrays.fill(numbers, 42);

转换字符串、链表

1
2
3
4
5
6
7
// toString(T[] a): 返回包含数组元素的字符串
int[] numbers = {1, 2, 3, 4, 5};
String arrayString = Arrays.toString(numbers);

// asList(T... a): 将数组转换为固定大小的列表
String[] names = {"John", "Alice", "Bob"};
List<String> nameList = Arrays.asList(names);

拷贝

1
2
3
// copyOf(T[] original, int newLength): 复制指定长度的数组
int[] numbers = {1, 2, 3, 4, 5};
int[] copiedNumbers = Arrays.copyOf(numbers, 3);

Date类

Date 类是 Java 中用于表示日期和时间的类,位于 java.util 包中。在 Java 8 及之前的版本中,Date 类是主要的日期时间处理类,但在 Java 8 引入了 java.time 包,推荐使用新的日期时间 API(java.time 包中的 LocalDate、LocalTime、LocalDateTime 等类)。尽管如此,我们仍然可以了解 Date 类的基本使用。

常用方法

getTime(): 返回自 1970 年 1 月 1 日 00:00:00 GMT 以来的毫秒数。
toString(): 返回日期对象的字符串表示。
after(Date when): 判断某个日期是否在指定日期之后
before(Date when):判断某个日期是否在指定日期之前
a.compareTo(b):对两个日期进行比较 如果a时间在b之后,则返回1 如果a时间在b之前,则返回-1 如果a==b,则返回0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test {
public static void main(String[] args) {
// 创建当前日期和时间的 Date 对象
Date currentDate = new Date();
// 输出当前日期和时间
System.out.println("Current Date and Time: " + currentDate);
// 获取毫秒表示的时间戳
long timestamp = currentDate.getTime();
System.out.println("Timestamp: " + timestamp);
// 通过时间戳创建 Date 对象
Date newDate = new Date(timestamp);
System.out.println("New Date: " + newDate);
}
}
// 输出结果
// Current Date and Time: Tue Jul 22 16:18:49 GMT+08:00 2025
// Timestamp: 1753172329875
// New Date: Tue Jul 22 16:18:49 GMT+08:00 2025

Date类的线程安全问题

Date类的缺点

  • 线程不安全:Date类可变,在多线程环境中使用时需要额外的同步措施。解决方案

    • 使用局部变量:局部变量不会被多个线程同时访问到。
    • 加锁:通过synchronized锁或者lock锁,保证线程同步。
    • ThreadLocalThreadLocal 可确保每个线程都能得到单独的一个 SimpleDateFormat 的对象,自然也就不存在竞争问题了
    • DateTimeFormatter:如果是Java8应用,可以使用DateTimeFormatter代替SimpleDateFormat,这是一个线程安全的格式化工具类
  • 获取时间不方便:Date类的年份是从 1900 年开始的,月份从 0 开始。

  • 没时区:Date类并不提供国际化,没有时区支持,因此Java引入了java.util.Calendar和java.util.TimeZone类,但他们同样存在上述所有的问题。

  • 需要格式化:需要搭配SimpleDateformat类格式化时间,而且SimpleDateformat类也是线程不安全的,如果使用线程安全的DateTimeFormatter类格式化时间,则需要JDK版本在1.8及以上。

格式化日期

SimpleDateformat类:线程不安全

SimpleDateformat类用于格式化和解析日期。

1
2
3
4
5
6
7
8
9
10
11
// throws只是把异常抛出,延迟处理。用trycatch处理比较好
public static void main(String[] args) throws ParseException {
// Date转String
Date d = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
String str = sdf.format(d);
System.out.println(str);
// String转Date
d = sdf.parse(str);
System.out.println(d);
}

日期格式模式:

SimpleDateFormat 使用一组模式字母来定义日期时间格式。以下是一些常见的模式字母:

  • y: 年份(如 “yy” 表示年份的后两位,”yyyy” 表示完整的年份)。

  • M: 月份(1-12,”MM” 表示两位数字,”MMM” 表示缩写,”MMMM” 表示全名)。

  • d: 日期(1-31,”dd” 表示两位数字)。

  • H: 小时(0-23,”HH” 表示两位数字)。

  • m: 分钟(0-59,”mm” 表示两位数字)。

  • s: 秒钟(0-59,”ss” 表示两位数字)。

DateTimeFormatter类:线程安全

DateTimeFormatter 类是 Java 8 引入的日期时间 API(java.time 包)的一部分,用于格式化和解析日期时间对象。

特点

  • 不可变且线程安全

  • 支持新的日期时间类

  • 适用于 Java 8 及以上版本。

  • 异常处理:在解析字符串时,如果字符串的格式与指定的模式不匹配,会抛出 DateTimeParseException 异常,因此需要异常处理。

常用方法

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
// 1.格式化日期时间:format(TemporalAccessor temporal): 格式化指定的日期时间对象。
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime dateTime = LocalDateTime.now();
String formattedDateTime = formatter.format(dateTime);
// 2.解析字符串为日期时间:parse(CharSequence text): 解析输入的文本,返回一个解析后的日期时间对象。
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String dateString = "2023-12-01 15:30:00";
LocalDateTime parsedDateTime = formatter.parse(dateString, LocalDateTime::from);
// 3.获取格式化/解析模式:toString(): 获取当前格式化/解析器的模式字符串。
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String pattern = formatter.toString(); // 返回 "yyyy-MM-dd HH:mm:ss"
// 4.将当前时间格式化为字符串
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class Test {
public static void main(String[] args) {
// 创建 DateTimeFormatter 对象,指定日期时间格式模式
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// 获取当前日期时间
LocalDateTime currentDateTime = LocalDateTime.now();
// 格式化日期时间为字符串
String formattedDateTime = currentDateTime.format(formatter);
System.out.println("Formatted Date and Time: " + formattedDateTime);
}
}

Math类

Math 类是数学工具类,提供了一系列用于执行基本数学运算的静态方法。

基本使用:

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 class Test {
public static void main(String[] args) {
// 基本数学运算
double absoluteValue = Math.abs(-5.5); // 返回绝对值,结果为 5.5
double ceilValue = Math.ceil(4.3); // 返回不小于参数的最小整数值,结果为 5.0
double floorValue = Math.floor(4.9); // 返回不大于参数的最大整数值,结果为 4.0
double maxValue = Math.max(10.2, 5.8); // 返回两个参数中的最大值,结果为 10.2
double minValue = Math.min(3.5, 7.1); // 返回两个参数中的最小值,结果为 3.5
long roundValue = Math.round(3.8); // 返回最接近参数的整数值,四舍五入,结果为 4
// 指数运算
double expValue = Math.exp(2.0); // 返回 e 的指数值,结果为 7.3890560989306495
double logValue = Math.log(10.0); // 返回以 e 为底的对数值,结果为 2.302585092994046
double powValue = Math.pow(2.0, 3.0); // 返回 2 的 3 次方,结果为 8.0
// 平方根和立方根
double sqrtValue = Math.sqrt(25.0); // 返回参数的平方根,结果为 5.0
double cbrtValue = Math.cbrt(27.0); // 返回参数的立方根,结果为 3.0
// 三角函数
double sinValue = Math.sin(Math.PI / 6); // 返回参数的正弦值,结果为 0.5
double cosValue = Math.cos(Math.PI / 3); // 返回参数的余弦值,结果为 0.5
double tanValue = Math.tan(Math.PI / 4); // 返回参数的正切值,结果为 1.0
// 角度和弧度转换
double degreesValue = Math.toDegrees(Math.PI / 2); // 将弧度转换为角度,结果为 90.0
double radiansValue = Math.toRadians(180.0); // 将角度转换为弧度,结果为 π
// 随机数生成
double randomValue = Math.random(); // 返回一个大于等于 0.0 且小于 1.0 的随机浮点数
}
}

Random

基本介绍

java.util.Random类:主要用于生成伪随机数。

伪随机:Random类产生的数字是伪随机的,在相同种子数(seed)下的相同次数产生的随机数是相同的。

代码示例 不指定种子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 创建 Random 对象
Random random = new Random();

//生成 0 到 10(不包括10)之间的随机整数
int num = random.nextInt(10);
//生成 -1 到 10(不包括10)之间的随机整数
int num2 = random.nextInt(10) - 1;
System.out.println("生成 0 到 10(不包括10)之间的随机整数:"+num);

// 生成随机浮点数:生成一个介于 0(包含)和 1(不包含)之间的浮点数
double randomDouble = random.nextDouble();
System.out.println("Random Double: " + randomDouble);

// 生成随机布尔值
boolean randomBoolean = random.nextBoolean();
System.out.println("Random Boolean: " + randomBoolean);

// 生成随机字节数组
byte[] randomBytes = new byte[5];
random.nextBytes(randomBytes);
System.out.println("Random Bytes: " + Arrays.toString(randomBytes));

代码示例 指定种子

相同种子数(seed)下的相同次数,产生的随机数是相同的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test {
public static void main(String[] args) {
// 指定种子为42
long seed = 42;
Random randomWithSeed = new Random(seed);
// 生成随机整数
int randomNumber1 = randomWithSeed.nextInt(100);
System.out.println("Random Number 1: " + randomNumber1);
// 再次生成随机整数,因为种子相同,结果应该相同
int randomNumber2 = randomWithSeed.nextInt(100);
System.out.println("Random Number 2: " + randomNumber2);
// 创建另一个 Random 对象,没有指定种子
Random randomWithoutSeed = new Random();
// 生成随机整数,因为没有指定种子,结果不受前面的影响
int randomNumber3 = randomWithoutSeed.nextInt(100);
System.out.println("Random Number 3: " + randomNumber3);
}
}