Java 总结二
- Java核心类
- 字符串和编码
- StringBuilder
- StringJoiner
- 包装类型
- JavaBean
- 枚举类
- 记录类
- BigInteger 和 BigDecimal
- 集合
- Collection
- List
- Map
- TreeMap
- Properties
- Set
- Queue
- Deque
- Stack
- Collections
Java核心类
字符串和编码
- Java字符串
String
是不可变对象; - 字符串操作不改变原字符串内容,而是返回新字符串;
- 常用的字符串操作:提取子串、查找、替换、大小写转换等;
- Java使用Unicode编码表示
String
和char
; - 转换编码就是将**
String
和byte[]
转换,需要指定编码**;转换为byte[]
时,始终优先考虑UTF-8
编码。 - 在Java中,
String
是一个引用类型,它本身也是一个class
- Java字符串的一个重要特点就是字符串不可变。这种不可变性是通过内部的
private final char[]
字段,以及没有任何修改char[]
的方法实现的。 - 结论:两个字符串比较,必须总是使用
equals()
方法。要忽略大小写比较,使用equalsIgnoreCase()
方法。==是用来比较两个实例是否指向相同的内存地址 new String(char[])
创建新的String
实例时,它并不会直接引用传入的char[]
数组,而是会复制一份,所以,修改外部的char[]
数组不会影响String
实例内部的char[]
数组,因为这是两个不同的数组。从String
的不变性设计可以看出,如果传入的对象有可能改变,我们需要复制而不是直接引用- 为了统一全球所有语言的编码,全球统一码联盟发布了
Unicode
编码,它把世界上主要语言都纳入同一个编码 - 因为英文字符的
Unicode
编码高字节总是00
,包含大量英文的文本会浪费空间,所以,出现了UTF-8
编码,它是一种变长编码,用来把固定长度的Unicode
编码变成1~4字节的变长编码。通过UTF-8
编码,英文字符'A'
的UTF-8
编码变为0x41
,正好和ASCII
码一致,而中文'中'
的UTF-8
编码为3字节0xe4b8ad
。
// 实际上字符串在String内部是通过一个char[]数组表示的
String s2 = new String(new char[] {'H', 'e', 'l', 'l', 'o', '!'});
// 是否包含字串
"Hello".contains("ll"); // true
"Hello".indexOf("l"); // 2
"Hello".lastIndexOf("l"); // 3
"Hello".startsWith("He"); // true
"Hello".endsWith("lo"); // true
// 提取子串
"Hello".substring(2); // "llo"
"Hello".substring(2, 4); // "ll"
// 使用trim()方法可以移除字符串首尾空白字符
" \tHello\r\n ".trim(); // "Hello"
"\u3000Hello\u3000".strip(); // "Hello"
// 替换子串
String s = "hello";
s.replace('l', 'w'); // "hewwo",所有字符'l'被替换为'w'
String s = "A,,B;C ,D";
s.replaceAll("[\\,\\;\\s]+", ","); // "A,B,C,D"
// 分隔字符串
String s = "A,B,C,D";
String[] ss = s.split("\\,"); // {"A", "B", "C", "D"}
// 拼接字符串
String[] arr = {"A", "B", "C"};
String s = String.join("***", arr); // "A***B***C"
// 格式化字符串
public class Main {
public static void main(String[] args) {
String s = "Hi %s, your score is %d!";
System.out.println(s.formatted("Alice", 80));
System.out.println(String.format("Hi %s, your score is %.2f!", "Bob", 59.5));
}
}
// 类型转换 重要 要把任意基本类型或引用类型转换为字符串
String.valueOf(123); // "123"
String.valueOf(45.67); // "45.67"
String.valueOf(true); // "true"
// 要把字符串转换为其他类型,就需要根据情况
int n1 = Integer.parseInt("123");
boolean b1 = Boolean.parseBoolean("true");
// String 和 char[] 转换
char[] cs = "Hello".toCharArray(); // String -> char[]
String s = new String(cs);
// 注意浅拷贝
public class Main {
public static void main(String[] args) {
int[] scores = new int[] { 88, 77, 51, 66 };
Score s = new Score(scores);
s.printScores(); // [88, 77, 51, 66]
scores[2] = 99;
s.printScores(); // [88, 77, 99, 66]
}
}
class Score {
private int[] scores;
public Score(int[] scores) {
this.scores = scores;
}
/*
深拷贝
public Score(int[] scores) {
this.scores = Arrays.copyOf(scores, scores.length);
this.scores = scores.clone();
}
*/
public void printScores() {
System.out.println(Arrays.toString(scores));
}
}
// 转换字符编码
byte[] b1 = "Hello".getBytes(); // 按系统默认编码转换,不推荐
byte[] b2 = "Hello".getBytes("UTF-8"); // 按UTF-8编码转换
byte[] b2 = "Hello".getBytes("GBK"); // 按GBK编码转换
byte[] b3 = "Hello".getBytes(StandardCharsets.UTF_8); // 按UTF-8编码转换
StringBuilder
- String添加字符在循环中,每次循环都会创建新的字符串对象,然后扔掉旧的字符串。这样,绝大部分字符串都是临时对象,不但浪费内存,还会影响GC效率。
- 为了能高效拼接字符串,Java标准库提供了
StringBuilder
,它是一个可变对象,可以预分配缓冲区,这样,往StringBuilder
中新增字符时,不会创建新的临时对象 - 定义的
append()
方法会返回this
,这样,就可以不断调用自身的其他方法。可以链式的添加 - 对于普通的字符串
+
操作,并不需要我们将其改写为StringBuilder
,因为Java编译器在编译时就自动把多个连续的+
操作编码为StringConcatFactory
的操作。在运行期,StringConcatFactory
会自动把字符串连接操作优化为数组复制或者StringBuilder
操作。 StringBuffer
,这是Java早期的一个StringBuilder
的线程安全版本,它通过同步来保证多个线程操作StringBuffer
也是安全的,但是同步会带来执行速度的下降。
StringBuilder sb = new StringBuilder(1024);
for (int i = 0; i < 1000; i++) {
sb.append(',');
sb.append(i);
}
String s = sb.toString();
StringJoiner
- 主要用来用分隔符拼接数组,可以指定开头和结尾
-
String
还提供了一个静态方法join()
,这个方法在内部使用了StringJoiner
来拼接字符串,在不需要指定“开头”和“结尾”的时候,用String.join()
更方便:
import java.util.StringJoiner;
public class Main {
public static void main(String[] args) {
String[] names = {"Bob", "Alice", "Grace"};
var sj = new StringJoiner(", ", "Hello ", "!");
for (String name : names) {
sj.add(name);
}
System.out.println(sj.toString());// Hello Bob, Alice, Grace!
}
}
String[] names = {"Bob", "Alice", "Grace"};
var s = String.join(", ", names);
包装类型
- Java的数据类型分两种:
- 基本类型:
byte
,short
,int
,long
,boolean
,float
,double
,char
- 引用类型:所有
class
和interface
类型
- 引用类型可以赋值为
null
,表示空,但基本类型不能赋值为null
- 这种直接把
int
变为Integer
的赋值写法,称为自动装箱(Auto Boxing),反过来,把Integer
变为int
的赋值写法,称为自动拆箱(Auto Unboxing)。注意:自动装箱和自动拆箱只发生在编译阶段,目的是为了少写代码。装箱和拆箱会影响执行效率,且拆箱时可能发生NullPointerException
; - 对两个
Integer
实例进行比较要特别注意:绝对不能用==
比较,因为Integer
是引用类型,必须使用equals()
比较: - 整数和浮点数的包装类型都继承自
Number
;包装类型提供了大量实用方法。
boolean | java.lang.Boolean |
byte | java.lang.Byte |
short | java.lang.Short |
int | java.lang.Integer |
long | java.lang.Long |
float | java.lang.Float |
double | java.lang.Double |
char | java.lang.Character |
// 互相转换
public class Main {
public static void main(String[] args) {
int i = 100;
// 通过new操作符创建Integer实例(不推荐使用,会有编译警告):
Integer n1 = new Integer(i);
// 通过静态方法valueOf(int)创建Integer实例:
Integer n2 = Integer.valueOf(i);
// 通过静态方法valueOf(String)创建Integer实例:
Integer n3 = Integer.valueOf("100");
// Integer 转为 int
System.out.println(n3.intValue());
}
}
// 自动装箱
int i = 100;
Integer n = Integer.valueOf(i);
int x = n.intValue();
/*
因为Integer.valueOf()可能始终返回同一个Integer实例,因此,在我们自己创建Integer的时候,以下两种方法:
方法1:Integer n = new Integer(100);
方法2:Integer n = Integer.valueOf(100);
方法2更好,因为方法1总是创建新的Integer实例,方法2把内部优化留给Integer的实现者去做,即使在当前版本没有优化,也有可能在下一个版本进行优化。
我们把能创建“新”对象的静态方法称为静态工厂方法。Integer.valueOf()就是静态工厂方法,它尽可能地返回缓存的实例以节省内存。
*/
public class Main {
public static void main(String[] args) {
Integer x = 127;
Integer y = 127;
Integer m = 99999;
Integer n = 99999;
System.out.println("x == y: " + (x==y)); // true
System.out.println("m == n: " + (m==n)); // false
System.out.println("x.equals(y): " + x.equals(y)); // true
System.out.println("m.equals(n): " + m.equals(n)); // true
}
}
/*
==比较,较小的两个相同的Integer返回true,较大的两个相同的Integer返回false,
这是因为Integer是不变类,编译器把Integer x = 127;自动变为Integer x = Integer.valueOf(127);,
为了节省内存,Integer.valueOf()对于较小的数,始终返回相同的实例,因此,==比较“恰好”为true,
但我们绝不能因为Java标准库的Integer内部有缓存优化就用==比较,必须用equals()方法比较两个Integer。
*/
JavaBean
- 有很多
class
的定义都符合这样的规范:
- 若干
private
实例字段; - 通过
public
方法来读写实例字段。
- JavaBean主要用来传递数据,即把一组数据组合成一个JavaBean便于传输。此外,JavaBean可以方便地被IDE工具分析,生成读写属性的代码,主要用在图形界面的可视化设计中。
- 点击右键,在弹出的菜单中选择“Source”,“Generate Getters and Setters”,在弹出的对话框中选中需要生成
getter
和setter
方法的字段,点击确定即可由IDE自动完成所有方法代码。 - 使用
Introspector.getBeanInfo()
可以获取属性列表。
// 如果读写方法符合以下这种命名规范: 那么这种class被称为JavaBean:
// 读方法:
public Type getXyz()
// 写方法:
public void setXyz(Type value)
/*
上面的字段是xyz,那么读写方法名分别以get和set开头,并且后接大写字母开头的字段名Xyz,
因此两个读写方法名分别是getXyz()和setXyz()。
boolean字段比较特殊,它的读方法一般命名为isXyz():
*/
public class Main {
public static void main(String[] args) throws Exception {
BeanInfo info = Introspector.getBeanInfo(Person.class);
for (PropertyDescriptor pd : info.getPropertyDescriptors()) {
System.out.println(pd.getName());
System.out.println(" " + pd.getReadMethod());
System.out.println(" " + pd.getWriteMethod());
}
}
}
/*
age
public int Person.getAge()
public void Person.setAge(int)
class
public final native java.lang.Class java.lang.Object.getClass()
null
name
public java.lang.String Person.getName()
public void Person.setName(java.lang.String)
*/
class Person {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
枚举类
- 因为
enum
类型的每个常量在JVM中只有一个唯一实例,所以可以直接用==
比较 - 定义的
enum
类型总是继承自java.lang.Enum
,且无法被继承; - 只能定义出
enum
的实例,而无法通过new
操作符创建enum
的实例; - 定义的每个实例都是引用类型的唯一实例;
- Java使用
enum
定义枚举类型,它被编译器编译为final class Xxx extends Enum { … }
; - 通过
name()
获取常量定义的字符串,注意不要使用toString()
; - 通过
ordinal()
返回常量定义的顺序(无实质意义); - 可以为
enum
编写构造方法、字段和方法 enum
的构造方法要声明为private
,字段强烈建议声明为final
;-
enum
适合用在switch
语句中。
public class Main {
public static void main(String[] args) {
Weekday day = Weekday.SUN;
if (day == Weekday.SAT || day == Weekday.SUN) {
System.out.println("Work at home!");
} else {
System.out.println("Work at office!");
}
}
}
enum Weekday {
SUN, MON, TUE, WED, THU, FRI, SAT;
}
String s = Weekday.SUN.name(); // "SUN"
int n = Weekday.MON.ordinal(); // 1
public class Main {
public static void main(String[] args) {
Weekday day = Weekday.SUN;
if (day.dayValue == 6 || day.dayValue == 0) {
System.out.println("Work at home!");
} else {
System.out.println("Work at office!");
}
}
}
enum Weekday {
MON(1), TUE(2), WED(3), THU(4), FRI(5), SAT(6), SUN(0);
public final int dayValue;
private Weekday(int dayValue) {
this.dayValue = dayValue;
}
}
public class Main {
public static void main(String[] args) {
Weekday day = Weekday.SUN;
if (day.dayValue == 6 || day.dayValue == 0) {
System.out.println("Today is " + day + ". Work at home!");
} else {
System.out.println("Today is " + day + ". Work at office!");
}
}
}
enum Weekday {
MON(1, "星期一"), TUE(2, "星期二"), WED(3, "星期三"), THU(4, "星期四"), FRI(5, "星期五"), SAT(6, "星期六"), SUN(0, "星期日");
public final int dayValue;
private final String chinese;
private Weekday(int dayValue, String chinese) {
this.dayValue = dayValue;
this.chinese = chinese;
}
// 可重写
@Override
public String toString() {
return this.chinese;
}
}
记录类
- record类
public record Point(int x, int y) {}
// 和下面的一样
public final class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int x() {
return this.x;
}
public int y() {
return this.y;
}
public String toString() {
return String.format("Point[x=%s, y=%s]", x, y);
}
public boolean equals(Object o) {
...
}
public int hashCode() {
...
}
}
public class Main {
public static void main(String[] args) {
Point p = new Point(123, 456);
System.out.println(p.x());
System.out.println(p.y());
System.out.println(p);
}
}
BigInteger 和 BigDecimal
BigInteger
用于表示任意大小的整数;BigInteger
是不变类,并且继承自Number
;将BigInteger
转换成基本类型时可使用longValueExact()
等方法保证结果准确。BigDecimal
用于表示精确的小数,常用于财务计算;BigDecimal
用scale()
表示小数位数- 调用
divideAndRemainder()
方法时,返回的数组包含两个BigDecimal
,分别是商和余数,其中商总是整数,余数不会大于除数 - 比较
BigDecimal
的值是否相等,必须使用compareTo()
而不能使用equals()
。 - Java提供的常用工具类有:
- Math:数学计算 Math.random() 0<= x < 1
- Random:生成伪随机数
- SecureRandom:生成安全的随机数
BigInteger bi = new BigInteger("1234567890");
System.out.println(bi.pow(5)); // 2867971860299718107233761438093672048294900000
BigDecimal n = new BigDecimal("12.75");
BigDecimal m = new BigDecimal("0.15");
BigDecimal[] dr = n.divideAndRemainder(m);
if (dr[1].signum() == 0) {
// n是m的整数倍
}
//必须使用compareTo()方法来比较,它根据两个值的大小分别返回负数、正数和0,分别表示小于、大于和等于。s
BigDecimal d1 = new BigDecimal("123.456");
BigDecimal d2 = new BigDecimal("123.45600");
System.out.println(d1.equals(d2)); // false,因为scale不同
System.out.println(d1.equals(d2.stripTrailingZeros())); // true,因为d2去除尾部0后scale变为2
System.out.println(d1.compareTo(d2)); // 0
Math.random() // 0<= x < 1
Random r = new Random();
r.nextInt(); // 2071575453,每次都不一样
r.nextInt(10); // 5,生成一个[0,10)之间的int
集合
Collection
- Java标准库自带的
java.util
包提供了集合类:Collection
,它是除Map
外所有其他集合类的根接口。Java的java.util
包主要提供了以下三种类型的集合:
-
List
:一种有序列表的集合,例如,按索引排列的Student
的List
; -
Set
:一种保证没有重复元素的集合,例如,所有无重复名称的Student
的Set
; -
Map
:一种通过键值(key-value)查找的映射表集合,例如,根据Student
的name
查找对应Student
的Map
。
- Java集合的设计有几个特点:一是实现了接口和实现类相分离,例如,有序表的接口是
List
,具体的实现类有ArrayList
,LinkedList
等,二是支持泛型,我们可以限制在一个集合中只能放入同一种数据类型的元素,例如: List list = new ArrayList<>(); // 只能放入String类型 - 最后,Java访问集合总是通过统一的方式——迭代器(Iterator)来实现,它最明显的好处在于无需知道集合内部元素是按什么方式存储的。
List
- 实现
List
接口并非只能通过数组(即ArrayList
的实现方式)来实现,另一种LinkedList
通过“链表”也实现了List接口。 - List允许添加重复值、允许添加null、
- 我们考察
List<E>
接口,可以看到几个主要的接口方法:
- 在末尾添加一个元素:
boolean add(E e)
- 在指定索引添加一个元素:
boolean add(int index, E e)
- 删除指定索引的元素:
E remove(int index)
- 删除某个元素:
boolean remove(Object e)
- 获取指定索引的元素:
E get(int index)
- 获取链表大小(包含元素的个数):
int size()
ArrayList | LinkedList | |
获取指定元素 | 速度很快 | 需要从头开始查找元素 |
添加元素到末尾 | 速度很快 | 速度很快 |
在指定位置添加/删除 | 需要移动元素 | 不需要移动元素 |
内存占用 | 少 | 较大 |
- 遍历
import java.util.Iterator;
import java.util.List;
public class Main {
public static void main(String[] args) {
// 给定元素快速创建List
List<String> list = List.of("apple", "pear", "banana");
for (Iterator<String> it = list.iterator(); it.hasNext(); ) {
String s = it.next();
System.out.println(s);
}
for (String s : list) {
System.out.println(s);
}
}
}
- List和Array的转换
- 如果传入的数组不够大,那么
List
内部会创建一个新的刚好够大的数组,填充后返回;如果传入的数组比List
元素还要多,那么填充完元素后,剩下的数组元素一律填充null
// 传入一个恰好大小的数组
Integer[] array = list.toArray(Integer[]::new);
toArray()
方法直接返回一个Object[]
数组,使用比较少会丢失类型信息- 是给
toArray(T[])
传入一个类型相同的Array
,List
内部自动把元素复制到传入的Array
中
//List 转 Array
public class Main {
public static void main(String[] args) {
List<Integer> list = List.of(12, 34, 56);
Integer[] array = list.toArray(new Integer[3]);
for (Integer n : array) {
System.out.println(n);
}
}
}
// Array转List的
Integer[] array = { 1, 2, 3 };
// 返回的只是一个只读List
List<Integer> list = List.of(array);
List
还提供了boolean contains(Object o)
方法来判断List
是否包含某个指定元素。此外,int indexOf(Object o)
方法可以返回某个元素的索引,如果元素不存在,就返回-1
。
- 因为
List
内部并不是通过==
判断两个元素是否相等,而是使用equals()
方法判断两个元素是否相等, - 正确使用
List
的contains()
、indexOf()
这些方法,放入的实例必须正确覆写equals()
方法,否则,放进去的实例,查找不到。我们之所以能正常放入String
、Integer
这些对象,是因为Java标准库定义的这些类已经正确实现了equals()
方法 equals()
方法的正确编写方法:
- 先确定实例“相等”的逻辑,即哪些字段相等,就认为实例相等;
- 用
instanceof
判断传入的待比较的Object
是不是当前类型,如果是,继续比较,否则,返回false
; - 对引用类型用
Objects.equals()
比较,对基本类型直接用==
比较。
public boolean equals(Object o) {
if (o instanceof Person) {
Person p = (Person) o;
return Objects.equals(this.name, p.name) && this.age == p.age;
}
return false;
}
Map
Map<K, V>
是一种键-值映射表,当我们调用put(K key, V value)
方法时,就把key
和value
做了映射并放入Map
。当我们调用V get(K key)
时,就可以通过key
获取到对应的value
。如果key
不存在,则返回null
。和List
类似,Map
也是一个接口,最常用的实现类是HashMap
。- 如果只是想查询某个
key
是否存在,可以调用boolean containsKey(K key)
方法。 - Map中不存在重复的key,因为放入相同的key,只会把原有的key-value对应的value给替换掉。
- 不保证顺序,存储的是key-value的映射关系
- 遍历
public class Main {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("apple", 123);
map.put("pear", 456);
map.put("banana", 789);
for (String key : map.keySet()) {
Integer value = map.get(key);
System.out.println(key + " = " + value);
}
for (Map.Entry<String, Integer> entry : map.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(key + " = " + value);
}
}
}
- equals 和 hashcode
- 因为在
Map
的内部,对key
做比较是通过equals()
实现的,这一点和List
查找元素需要正确覆写equals()
是一样的,即正确使用Map
必须保证:作为key
的对象必须正确覆写equals()
方法。 - 我们经常使用
String
作为key
,因为String
已经正确覆写了equals()
方法。但如果我们放入的key
是一个自己写的类,就必须保证正确覆写了equals()
方法。 - 通过
key
计算索引的方式就是调用key
对象的hashCode()
方法,它返回一个int
整数。HashMap
正是通过这个方法直接定位key
对应的value
的索引,继而直接返回value
。 - 一个类如果覆写了
equals()
,就必须覆写hashCode()
,并且覆写规则是:
- 如果
equals()
返回true
,则hashCode()
返回值必须相等; - 如果
equals()
返回false
,则hashCode()
返回值尽量不要相等。 - 实现
hashCode()
方法可以通过Objects.hashCode()
辅助方法实现。
int hashCode() {
return Objects.hash(firstName, lastName, age);
}
TreeMap
HashMap
是一种以空间换时间的映射表,它的实现原理决定了内部的Key是无序的,即遍历HashMap
的Key时,其顺序是不可预测的(但每个Key都会遍历一次且仅遍历一次)。还有一种Map
,它在内部会对Key进行排序,这种Map
就是SortedMap
。注意到SortedMap
是接口,它的实现类是TreeMap
。SortedMap
保证遍历时以Key的顺序来进行排序。- 使用
TreeMap
时,放入的Key必须实现Comparable
接口。String
、Integer
这些类已经实现了Comparable
接口,因此可以直接作为Key使用。作为Value的对象则没有任何要求。如果作为Key的class没有实现Comparable
接口,那么,必须在创建TreeMap
时同时指定一个自定义排序算法: TreeMap
不使用equals()
和hashCode()
。
public class Main {
public static void main(String[] args) {
Map<Person, Integer> map = new TreeMap<>(new Comparator<Person>() {
public int compare(Person p1, Person p2) {
return p1.name.compareTo(p2.name);
}
});
map.put(new Person("Tom"), 1);
map.put(new Person("Bob"), 2);
map.put(new Person("Lily"), 3);
for (Person key : map.keySet()) {
System.out.println(key);
}
// {Person: Bob}, {Person: Lily}, {Person: Tom}
System.out.println(map.get(new Person("Bob"))); // 2
}
}
class Person {
public String name;
Person(String name) {
this.name = name;
}
public String toString() {
return "{Person: " + name + "}";
}
}
Properties
- 配置文件,它的Key-Value一般都是
String
-String
类型的,因此我们完全可以用Map<String, String>
来表示它,Properties
内部本质上是一个Hashtable
,但我们只需要用到Properties
自身关于读写配置的接口 - 用
Properties
读取配置文件,一共有三步:
- 创建
Properties
实例; - 调用
load()
读取文件; - 调用
getProperty()
获取配置。
// 典型的配置文件
# setting.properties
last_open_file=/data/hello.txt
auto_save_interval=60
// 读取文件
String f = "setting.properties";
Properties props = new Properties();
props.load(new java.io.FileInputStream(f));
String filepath = props.getProperty("last_open_file");
String interval = props.getProperty("auto_save_interval", "120");
// 解决中文乱码的问题
Properties props = new Properties();
props.load(new FileReader("settings.properties", StandardCharsets.UTF_8));
// 写入配置文件
Properties props = new Properties();
props.setProperty("url", "http://www.liaoxuefeng.com");
props.setProperty("language", "Java");
props.store(new FileOutputStream("C:\\conf\\setting.properties"), "这是写入的properties注释");
Set
Set
实际上相当于只存储key、不存储value的Map
。我们经常用Set
用于去除重复元素。- 因为放入
Set
的元素和Map
的key类似,都要正确实现equals()
和hashCode()
方法,否则该元素无法正确地放入Set
。 - 最常用的
Set
实现类是HashSet
,实际上,HashSet
仅仅是对HashMap
的一个简单封装,它的核心代码如下: Set
用于存储不重复的元素集合,它主要提供以下几个方法:
- 将元素添加进
Set<E>
:boolean add(E e)
- 将元素从
Set<E>
删除:boolean remove(Object e)
- 判断是否包含元素:
boolean contains(Object e)
Set
接口并不保证有序,而SortedSet
接口则保证元素是有序的:
-
HashSet
是无序的,因为它实现了Set
接口,并没有实现SortedSet
接口; -
TreeSet
是有序的,因为它实现了SortedSet
接口。
public class Main {
public static void main(String[] args) {
Set<String> set = new HashSet<>();
System.out.println(set.add("abc")); // true
System.out.println(set.add("xyz")); // true
System.out.println(set.add("xyz")); // false,添加失败,因为元素已存在
System.out.println(set.contains("xyz")); // true,元素存在
System.out.println(set.size()); // 2,一共两个元素
}
}
Queue
- 队列
Queue
实现了一个先进先出(FIFO)的数据结构:
- 通过
add()
/offer()
方法将元素添加到队尾; - 通过
remove()
/poll()
从队首获取元素并删除; - 通过
element()
/peek()
从队首获取元素但不删除。
- 要避免把
null
添加到队列。
throw Exception | 返回false或null | |
添加元素到队尾 | add(E e) | boolean offer(E e) |
取队首元素并删除 | E remove() | E poll() |
取队首元素但不删除 | E element() | E peek() |
public class Main {
public static void main(String[] args) {
Queue<String> q = new LinkedList<>();
// 添加3个元素到队列:
q.offer("apple");
q.offer("pear");
q.offer("banana");
// 从队列取出元素:
System.out.println(q.poll()); // apple
System.out.println(q.poll()); // pear
System.out.println(q.poll()); // banana
System.out.println(q.poll()); // null,因为队列是空的
}
}
PriorityQueue
实现了一个优先队列:从队首获取元素时,总是获取优先级最高的元素。PriorityQueue
默认按元素比较的顺序排序(必须实现Comparable
接口),也可以通过Comparator
自定义排序算法(元素就不必实现Comparable
接口)。
public class Main {
public static void main(String[] args) {
Queue<User> q = new PriorityQueue<>(new UserComparator());
// 添加3个元素到队列:
q.offer(new User("Bob", "A1"));
q.offer(new User("Alice", "A2"));
q.offer(new User("Boss", "V1"));
System.out.println(q.poll()); // Boss/V1
System.out.println(q.poll()); // Bob/A1
System.out.println(q.poll()); // Alice/A2
System.out.println(q.poll()); // null,因为队列为空
}
}
class UserComparator implements Comparator<User> {
public int compare(User u1, User u2) {
if (u1.number.charAt(0) == u2.number.charAt(0)) {
// 如果两人的号都是A开头或者都是V开头,比较号的大小:
return u1.number.compareTo(u2.number);
}
if (u1.number.charAt(0) == 'V') {
// u1的号码是V开头,优先级高:
return -1;
} else {
return 1;
}
}
}
class User {
public final String name;
public final String number;
public User(String name, String number) {
this.name = name;
this.number = number;
}
public String toString() {
return name + "/" + number;
}
}
Deque
Queue | Deque | |
添加元素到队尾 | add(E e) / offer(E e) | addLast(E e) / offerLast(E e) |
取队首元素并删除 | E remove() / E poll() | E removeFirst() / E pollFirst() |
取队首元素但不删除 | E element() / E peek() | E getFirst() / E peekFirst() |
添加元素到队首 | 无 | addFirst(E e) / offerFirst(E e) |
取队尾元素并删除 | 无 | E removeLast() / E pollLast() |
取队尾元素但不删除 | 无 | E getLast() / E peekLast() |
// 推荐的写法:
Deque<String> d2 = new LinkedList<>();
d2.offerLast("z");
Stack
- 在Java中,我们用
Deque
可以实现Stack
的功能:
- 把元素压栈:
push(E)
/addFirst(E)
; - 把栈顶的元素“弹出”:
pop()
/removeFirst()
; - 取栈顶元素但不弹出:
peek()
/peekFirst()
。
Collections
-
Collections
是JDK提供的工具类,同样位于java.util
包中。它提供了一系列静态方法,能更方便地操作各种集合 - 创建空集合,
Collections
提供了一系列方法来创建空集合:
- 创建空List:
List<T> emptyList()
- 创建空Map:
Map<K, V> emptyMap()
- 创建空Set:
Set<T> emptySet()
- Collections
可以对
List进行排序。因为排序会直接修改
List元素的位置,因此必须传入可变
List -
Collections
提供了洗牌算法,即传入一个有序的List
,可以随机打乱List
内部元素的顺序,效果相当于让计算机洗牌 Collections.shuffle(list)