1. 面向对象
1.1 基本概念
OOD:代表“面向对象设计”(Object-Oriented Design)是一种编程设计方法学,基于面向对象编程(OOP)的概念和原则,如封装、继承和多态。OOD的核心在于使用对象(具有属性和行为的实体)来模拟真实世界中的事物和交互。
OOP:代表面向对象编程(Object-Oriented Programming)是一种编程范式,它使用“对象”来设计软件。在OOP中,对象是包含数据和方法的实体,用于模拟现实世界中的事物和概念。面向对象编程的主要特点包括封装、继承和多态。
1.2 三大特性
-
封装(Encapsulation)
封装是将数据(属性)和操作这些数据的代码(方法)捆绑在一起的过程。这样做的目的是隐藏对象的内部细节和实现,只暴露必要的操作接口。这有助于降低代码复杂性,提高安全性。
例子: 想象一下一个咖啡机。你不需要知道它内部是如何工作的,你只需要知道如何操作它(比如按下按钮来制作咖啡)。咖啡机的内部机制被封装起来,而用户界面提供了与之交互的方法。
- 优点:提高数据安全性,简化接口,降低耦合度,重用性和模块化。
- 缺点:过度封装,可能导致性能开销,增加代码复杂性,测试有难度。
-
继承(Inheritance)
继承是一种允许新创建的对象继承现有对象的属性和方法的机制。这有助于减少代码重复,增强代码的可重用性。
例子: 假设我们有一个“动物”类,包含所有动物共有的属性和方法,如“呼吸”和“移动”。我们可以创建一个“狗”类,继承自“动物”类。这样,“狗”类就自动拥有了“呼吸”和“移动”的能力,而我们也可以为“狗”添加专有的属性和方法,如“汪汪叫”。
- 优点:减少代码重复,建立继承体系。
- 缺点:继承体系容易过于复杂、混乱,重写功能可能导致方法作用不明确。
可以使用组合关系代替继承关系。
-
多态(Polymorphism)
多态是指允许不同类的对象对同一消息做出响应的能力,即同一个接口可以被不同的对象以不同的方式实现。一个对象在运行时的多种形态。(接口与实现类)
例子: 继续上面的“动物”类的例子,假设有一个方法叫“发出声音”。不同的动物发出的声音是不同的,狗会“汪汪叫”,猫会“喵喵叫”。多态允许我们对不同的动物对象调用同一个“发出声音”的方法,但每个动物会以它自己的方式来响应。
- 优点:解耦
- 缺点:初学者难以理解,动态绑定,影响性能,测试复杂性
结合例子理解面向对象思想
想象你正在开发一个模拟动物园的软件。你创建了一个基类“动物”,它定义了所有动物共有的属性和行为,如年龄、体重和“移动”的方法。然后你创建了几个继承自“动物”的子类,如“狮子”、“大象”和“长颈鹿”,每个子类有其特有的属性和行为。例如,“狮子”类可能有一个“吼叫”的方法,而“长颈鹿”类可能有一个“吃树叶”的方法。这里,封装使得每个类的实现细节对外部是隐藏的,继承让你可以重用“动物”类的代码,而多态允许你以统一的方式处理不同类型的动物。
面向对象编程的这些特性使得代码更易于理解、维护和扩展。通过模拟现实世界的实体和概念,它允许开发者以更自然的方式思考和解决问题。
1.3 重载与重写
重载(Overloading)和重写(Overriding)是面向对象编程中两个基本且重要的概念,尽管它们听起来相似,但它们在功能和用途上有显著的区别。
重载(Overloading)
重载是指在同一个类中有多个同名方法,但这些方法的参数列表不同(参数的类型、个数或顺序不同)。
特点
- 发生在同一个类中: 重载发生在一个类的内部。
- 方法名相同: 重载的方法共享同一个名称。
- 参数列表不同: 重载的方法必须有不同的参数列表。
- 返回类型可以不同: 重载的方法可以有不同的返回类型,但仅改变返回类型不足以构成重载。
- 编译时决定: Java编译器根据方法签名在编译时就确定了要调用哪个方法。
示例
public class Example {public void display(String s) {System.out.println("String: " + s);}public void display(String s, int n) {for (int i = 0; i < n; i++) {System.out.println("String: " + s);}}
}
在这个例子中,display
方法被重载了。两个display
方法有相同的名称,但参数列表不同。
重写(Overriding)
重写是指子类重新定义父类中的某个方法的实现。子类的方法必须和父类被重写的方法具有相同的方法名称、返回类型和参数列表。
特点
- 发生在父子类之间: 重写发生在继承关系的两个类之间。
- 方法名、返回类型和参数列表相同: 重写的方法在子类中必须与父类中的方法完全相同。
- 访问权限不能更严格: 重写的方法不能比父类方法具有更严格的访问权限。
- 运行时决定: 哪个方法被调用是在运行时基于对象的实际类型决定的。
示例
class Animal {public void makeSound() {System.out.println("Some sound");}
}class Dog extends Animal {@Overridepublic void makeSound() {System.out.println("Bark");}
}class Cat extends Animal {@Overridepublic void makeSound() {System.out.println("Meow");}
}
在这个例子中,Dog
和 Cat
类重写了它们从 Animal
类继承的 makeSound
方法。
总结
- 重载 是指同一个类中的多个同名方法具有不同的参数列表。
- 重写 是子类重定义父类的某个方法。
重载使得同一个方法可以根据不同的参数执行不同的功能,而重写则是用于实现多态,即同一个接口的不同实现。
1.4 this和super
在Java中,super
和 this
关键字用于引用对象的当前实例和其父类的属性或方法。
this
关键字
this
用于引用当前对象的实例。它通常用于以下几个方面:
- 区分实例变量和局部变量: 当方法的参数名与类的实例变量名相同,可以使用
this
来区分它们。 - 在一个构造器中调用另一个构造器: 使用
this()
调用当前类中的另一个构造器。 - 返回当前对象的引用: 在方法中返回当前对象。
示例代码
public class MyClass {private int var;public MyClass(int var) {this.var = var; // 使用 this 区分实例变量和构造器参数}public void setVar(int var) {this.var = var; // 使用 this 区分实例变量和方法参数}public int getVar() {return this.var; // 使用 this 引用当前对象的变量}public MyClass getInstance() {return this; // 返回当前对象实例}
}
super
关键字
super
用于引用当前对象的父类。这在子类覆盖父类的方法(重写)或者想要访问父类的属性时非常有用。
- 调用父类的构造器: 使用
super()
调用父类的构造器。 - 访问被子类覆盖的父类方法: 使用
super.methodName()
访问。 - 访问父类的属性: 如果子类和父类有同名的属性,可以使用
super
来引用父类的属性。
示例代码
class ParentClass {protected String name;public ParentClass(String name) {this.name = name;}public void display() {System.out.println("Name in ParentClass: " + name);}
}class ChildClass extends ParentClass {public ChildClass(String name) {super(name); // 调用父类的构造器}@Overridepublic void display() {super.display(); // 调用父类的 display 方法System.out.println("Name in ChildClass: " + name);}
}public class Test {public static void main(String[] args) {ChildClass obj = new ChildClass("Test");obj.display();}
}
在这个例子中,ChildClass
继承自 ParentClass
。ChildClass
的构造器通过 super(name)
调用了 ParentClass
的构造器。同样,ChildClass
的 display()
方法覆盖了 ParentClass
的 display()
方法,并通过 super.display()
调用了父类的方法。
总结
this
是对当前对象实例的引用。super
是对当前对象的父类的引用。
它们都用于访问对象的属性和方法,但 this
引用当前类的成员,而 super
引用父类的成员。这在处理继承时特别有用,可以帮助区分子类和父类中同名的属性和方法。
1.5 继承和实现的区别
- 继承 是“是一个(is-a)”关系,例如 Dog 是一个 Animal。
- 实现 是“能做什么(can-do)”关系,例如 Circle 能够进行绘制(Drawable)。
继承和实现都是实现代码重用和功能扩展的重要手段。在Java中,继承是单继承,即每个类只能继承一个父类,而实现则是多重的,即一个类可以实现多个接口。这两种机制共同工作,为Java编程提供了强大的灵活性和表达力。
1.6 Java Bean与内省
-
Java Bean
-
类必须public 修饰
-
必须有一个无参构造器
-
所有字段私有化且提供getter/setter方法
-
扩展:
字段:类中的变量称之为字段
属性:类中的getter/setter只要存在之一,getXxx中的xxx就是属性名称
-
-
内省机制
Java提供的一套更便于操作Java Bean属性的API(相较于反射更容易操作Java Bean属性的API)。
2. 常用类
2.1 String
Java中的String
类包含许多用于操作字符串的常用方法。以下是一些常用的String
方法及其简要说明:
-
length()
- 返回字符串的长度。
- 示例:
"hello".length()
返回5
。
-
charAt(int index)
- 返回指定索引处的字符。
- 示例:
"hello".charAt(1)
返回'e'
。
-
substring(int beginIndex), substring(int beginIndex, int endIndex)
- 返回字符串的一个子串。
- 示例:
"hello".substring(1)
返回"ello"
;"hello".substring(1, 3)
返回"el"
。
-
contains(CharSequence s)
- 检查字符串是否包含指定的字符序列。
- 示例:
"hello".contains("ll")
返回true
。
-
equals(Object anotherObject), equalsIgnoreCase(String anotherString)
- 比较两个字符串是否相等。
equalsIgnoreCase
忽略大小写。 - 示例:
"Hello".equals("hello")
返回false
;"Hello".equalsIgnoreCase("hello")
返回true
。
- 比较两个字符串是否相等。
-
startsWith(String prefix), endsWith(String suffix)
- 检查字符串是否以指定的前缀开始或以指定的后缀结束。
- 示例:
"hello".startsWith("he")
返回true
;"hello".endsWith("lo")
返回true
。
-
toLowerCase(), toUpperCase()
- 将字符串转换为全部小写或大写。
- 示例:
"Hello".toLowerCase()
返回"hello"
;"hello".toUpperCase()
返回"HELLO"
。
-
trim()
- 返回一个新字符串,它是通过移除原始字符串开头和结尾的空白字符获得的。
- 示例:
" hello ".trim()
返回"hello"
。
-
replace(char oldChar, char newChar), replace(CharSequence target, CharSequence replacement)
- 替换字符串中的字符或字符序列。
- 示例:
"hello".replace('l', 'p')
返回"heppo"
;"hello".replace("ll", "yy")
返回"heyyo"
。
-
split(String regex)
- 根据匹配给定的正则表达式来分割字符串。
- 示例:
"a,b,c".split(",")
返回数组["a", "b", "c"]
。
-
indexOf(int ch), indexOf(String str), lastIndexOf(int ch), lastIndexOf(String str)
- 返回指定字符或字符串在该字符串中首次出现处的索引,
lastIndexOf
返回最后一次出现的索引。 - 示例:
"hello".indexOf('l')
返回2
;"hello".lastIndexOf('l')
返回3
。
- 返回指定字符或字符串在该字符串中首次出现处的索引,
-
concat(String str)
- 将指定字符串连接到此字符串的末尾。
- 示例:
"Hello, ".concat("world!")
返回"Hello, world!"
。
这些方法是处理字符串时非常常用的操作,能够满足大多数基本的字符串处理需求。
2.2 对象比较
在Java中,Comparable
和 Comparator
接口都用于实现对象的排序,但它们在用法和目的上有一些关键的区别。
2.2.1 Comparable 接口
Comparable
接口用于定义对象自然排序的方式。类通过实现 Comparable
接口的 compareTo
方法来定义其对象的排序逻辑。
- 自然排序: 当一个类实现了
Comparable
接口,它的对象集合可以直接使用Collections.sort
或Arrays.sort
进行排序。 - compareTo 方法: 这个方法返回一个整数,表示调用者对象相对于传入参数的排序顺序。
示例
public class Person implements Comparable<Person> {private int age;public Person(int age) {this.age = age;}@Overridepublic int compareTo(Person other) {return this.age - other.age; // 年龄的升序排序}
}
在这个例子中,Person
类实现了 Comparable
接口,以年龄进行自然排序。
2.2.2 Comparator 接口
Comparator
接口用于定义一个外部的排序逻辑,它允许定义多种排序规则,或者在类没有实现 Comparable
接口的情况下提供排序逻辑。
- 自定义排序: 可以创建多个不同的
Comparator
实现类来定义不同的排序规则。 - compare 方法: 这个方法接受两个参数,返回一个整数表示它们的排序顺序。
示例
public class AgeComparator implements Comparator<Person> {@Overridepublic int compare(Person p1, Person p2) {return p1.getAge() - p2.getAge(); // 年龄的升序排序}
}
在这个例子中,AgeComparator
提供了 Person
对象按年龄排序的规则。
2.2.3 主要区别
-
实现位置:
Comparable
是在类的内部实现的,定义了对象的自然排序顺序。Comparator
是在类的外部实现的,定义了一种外部的排序规则。
-
方法数量:
Comparable
只包含一个方法compareTo
。Comparator
包含一个方法compare
。
-
控制权:
- 使用
Comparable
时,类的设计者控制着其排序逻辑。 - 使用
Comparator
时,类的用户可以定义自己的排序逻辑。
- 使用
-
灵活性:
Comparable
提供单一的自然排序,不够灵活。Comparator
可以提供多种排序逻辑,更加灵活。
根据不同的需求选择使用 Comparable
或 Comparator
。如果一个类有一个明确的、自然的排序逻辑(如数字、字母顺序),则使用 Comparable
;如果需要多种排序方式,或者类本身不具备自然排序逻辑,或者你无法修改类的源代码,那么使用 Comparator
是更好的选择。
2.3 单例模式
- 懒汉式:用到才会创建对象
- 饿汉式:直接创建对象,直接使用(普通版 -> 同步锁 -> 双重检查 -> volatile -> 枚举 -> 静态内部类)
双检加锁机制
if(){ // 第一次检查synchronized(obj){ // 检查if(){ // 第二次检查}}
}
3. 集合
3.1 List
3.1.1 ArrayList
ArrayList
是 Java 中一个非常重要的集合类,属于 Java 集合框架(Java Collections Framework)的一部分。它基于动态数组的概念实现,提供了一种以数组方式存储元素的列表实现。
主要特点
-
动态数组:
ArrayList
内部使用数组来存储元素。当元素超出当前数组容量时,ArrayList
会创建一个新的更大的数组,并将所有元素复制到这个新数组中(称为“扩容”)。 -
随机访问: 由于基于数组实现,
ArrayList
提供快速的随机访问功能,可以通过索引在常数时间内访问元素(get(int index)
和set(int index, E element)
方法)。 -
有序且可重复:
ArrayList
保持元素插入的顺序,并允许插入重复的元素。 -
非同步的:
ArrayList
不是线程安全的。如果在多线程环境中使用,需要外部同步。
主要方法
-
添加元素:
add(E e)
方法用于在列表末尾添加一个元素,add(int index, E element)
方法用于在指定位置添加元素。 -
访问元素:
get(int index)
方法用于访问指定位置的元素。 -
设置元素:
set(int index, E element)
方法用于替换指定位置的元素。 -
删除元素:
remove(int index)
方法用于移除指定位置的元素,remove(Object o)
方法用于移除第一次出现的指定元素。 -
列表大小:
size()
方法返回列表中的元素数量。 -
遍历列表: 可以通过迭代器(
Iterator
)或增强的 for 循环来遍历ArrayList
。 -
判断是否包含:
contains(Object o)
方法用于判断列表是否包含指定的元素。
示例代码
import java.util.ArrayList;public class ArrayListExample {public static void main(String[] args) {ArrayList<String> list = new ArrayList<>();list.add("Apple");list.add("Banana");list.add("Cherry");System.out.println("ArrayList: " + list);String fruit = list.get(1);System.out.println("Accessed Element: " + fruit);list.set(1, "Blueberry");System.out.println("Modified ArrayList: " + list);list.remove("Cherry");System.out.println("ArrayList after removal: " + list);}
}
使用注意事项
-
自动扩容机制:
ArrayList
的自动扩容可能会影响性能。如果预先知道存储元素的数量,可以通过初始化时指定容量来优化性能。 -
非线程安全: 在多线程环境下,建议使用
Collections.synchronizedList
方法来同步ArrayList
,或者使用线程安全的替代品如Vector
或CopyOnWriteArrayList
。
ArrayList
由于其灵活性和易用性,是 Java 中使用最广泛的集合之一。它是实现列表功能的优选,特别是当需要频繁访问列表中的元素时。
3.1.2 LinkedList
LinkedList
是 Java 集合框架的一部分,是一个基于双向链表实现的列表类。与基于动态数组实现的 ArrayList
相比,LinkedList
提供了更好的插入和删除元素的性能,但在随机访问元素方面表现较差。
主要特点
-
链表数据结构:
LinkedList
内部使用双向链表来存储元素。每个元素(节点)都包含数据和两个引用,一个指向前一个元素,一个指向后一个元素。 -
动态大小:
LinkedList
的大小是动态的,可以根据需要添加或删除节点。 -
顺序访问: 访问元素时,需要从头节点或尾节点开始遍历,因此随机访问效率低。
-
实现了
List
和Deque
接口:LinkedList
不仅实现了List
接口,还实现了双端队列(Deque
)接口,因此它还可以作为栈、队列或双端队列使用。
主要方法
-
添加元素:
add(E e)
在列表末尾添加元素,add(int index, E element)
在指定位置添加元素,addFirst(E e)
和addLast(E e)
分别在列表头部和尾部添加元素。 -
访问元素:
get(int index)
获取指定位置的元素,getFirst()
和getLast()
分别获取第一个和最后一个元素。 -
删除元素:
remove(int index)
移除指定位置的元素,remove(Object o)
移除第一次出现的指定元素,removeFirst()
和removeLast()
分别移除第一个和最后一个元素。 -
列表大小:
size()
方法返回列表中的元素数量。 -
判断和搜索:
contains(Object o)
判断列表是否包含指定元素,indexOf(Object o)
和lastIndexOf(Object o)
分别返回元素首次和最后一次出现的位置。
示例代码
import java.util.LinkedList;public class LinkedListExample {public static void main(String[] args) {LinkedList<String> list = new LinkedList<>();list.add("Apple");list.add("Banana");list.addFirst("Strawberry");list.addLast("Cherry");System.out.println("LinkedList: " + list);String firstElement = list.getFirst();System.out.println("First Element: " + firstElement);list.removeLast();System.out.println("LinkedList after removing last: " + list);}
}
使用注意事项
-
性能考虑: 对于需要频繁插入和删除元素的场景,
LinkedList
是一个好选择。但对于需要频繁随机访问元素的场景,ArrayList
可能更合适。 -
内存占用: 由于每个元素都需要额外的空间存储前后节点的引用,
LinkedList
比ArrayList
占用更多内存。 -
迭代器: 使用
ListIterator
可以在LinkedList
中向前和向后遍历。
LinkedList
由于其在列表两端插入和删除操作上的高效性,经常被用作队列、栈或双端队列。但是,如果你需要频繁地随机访问列表中的元素,那么 ArrayList
可能是一个更好的选择。
3.1.3 区别及场景
ArrayList
和 LinkedList
都是 Java 中的 List
接口的实现,但它们在内部数据结构和性能特性上有显著的不同。了解这些差异有助于选择最适合特定场景的数据结构。
ArrayList
ArrayList
基于动态数组实现,适用于频繁的读取操作。
特点
-
随机访问快:
ArrayList
支持快速的随机访问,因为它允许直接通过索引访问元素(时间复杂度为 O(1))。 -
修改慢: 在列表中间插入或删除元素比较慢,因为这可能涉及移动元素以维护数组的连续性(时间复杂度为 O(n))。
-
内存占用: 相对较高的内存开销,因为它在数组的基础上维护容量(capacity),且扩容操作涉及复制元素到新的数组。
使用场景
- 频繁地通过索引访问元素,如随机访问。
- 添加元素通常发生在列表末尾。
- 需要频繁地调整列表大小。
LinkedList
LinkedList
基于双向链表实现,适用于频繁的插入和删除操作。
特点
-
插入和删除快: 在
LinkedList
中添加或删除元素不需要移动其它元素(时间复杂度为 O(1)),特别是在列表的开头或结尾。 -
随机访问慢: 访问元素需要从头节点或尾节点开始遍历,因此随机访问效率较低(时间复杂度为 O(n))。
-
内存占用: 每个元素都需要额外的空间来存储前后节点的引用,因此内存占用比
ArrayList
更高。
使用场景
- 需要频繁地在列表中间添加或删除元素。
- 实现栈、队列或双端队列。
- 不需要频繁地随机访问元素。
示例
-
ArrayList 示例:
- 实现一个数字列表,需要频繁地读取和更新元素,但很少在中间插入或删除元素。
- 代码示例:
List<Integer> numbers = new ArrayList<>(); numbers.add(1); numbers.add(2); int number = numbers.get(0); // 快速随机访问
-
LinkedList 示例:
- 实现一个待办事项列表,需要频繁地在列表的开头或中间添加和删除任务。
- 代码示例:
List<String> todoList = new LinkedList<>(); todoList.addFirst("Wake up"); todoList.addLast("Go to bed"); todoList.removeFirst(); // 快速插入和删除
总结来说,ArrayList
适合读取操作频繁的场景,而 LinkedList
更适合于插入和删除操作频繁的场景。正确选择两者之一可以显著提高程序的性能和效率。
3.2 Set
3.2.1 HashSet
HashSet
是 Java 中一个非常重要的集合类,它实现了 Set
接口。HashSet
内部是基于 HashMap
实现的,它提供了对集合元素的快速查找,并确保集合中不会有重复元素。
主要特点
-
唯一性:
HashSet
不允许存储重复元素,每个值在HashSet
中只能出现一次。 -
无序集合:
HashSet
不保证集合的迭代顺序;它的顺序可能随时间的推移而变化。 -
空值:
HashSet
允许存储一个null
元素。 -
性能: 提供了常数时间复杂度的添加、删除、包含以及大小操作,假设哈希函数将元素正确地分散在桶中。
主要方法
-
添加元素:
add(E e)
方法用于向HashSet
中添加元素。 -
删除元素:
remove(Object o)
方法用于从HashSet
中删除元素。 -
查找元素:
contains(Object o)
方法用于检查HashSet
是否包含特定元素。 -
集合大小:
size()
方法用于获取HashSet
中的元素数量。 -
清空集合:
clear()
方法用于移除HashSet
中所有元素。 -
遍历集合: 可以使用增强的 for 循环或迭代器来遍历
HashSet
。
示例代码
import java.util.HashSet;
import java.util.Set;public class HashSetExample {public static void main(String[] args) {Set<String> set = new HashSet<>();set.add("Apple");set.add("Banana");set.add("Cherry");set.add("Apple"); // 重复元素,不会被添加System.out.println("HashSet: " + set);if (set.contains("Banana")) {System.out.println("HashSet contains Banana");}set.remove("Banana");System.out.println("HashSet after removal: " + set);}
}
使用注意事项
-
哈希函数:
HashSet
的性能依赖于哈希函数的质量。一个好的哈希函数能够均匀地分布元素,减少哈希冲突。 -
null
元素:HashSet
允许存储一个null
元素,但要注意使用时的空指针异常。 -
迭代性能: 迭代
HashSet
的时间复杂度与HashSet
的容量成正比,因此在设置初始容量时要考虑到迭代性能。 -
线程安全:
HashSet
不是线程安全的。如果在多线程环境中使用,需要进行外部同步。
HashSet
由于其在处理大量数据时的高效性和简便性,是实现集合操作的首选,特别是当不需要元素排序或重复元素时。
3.2.2 TreeSet
TreeSet
是 Java 集合框架的一部分,它实现了 SortedSet
接口。与 HashSet
不同,TreeSet
是基于红黑树(一种自平衡二叉搜索树)实现的。TreeSet
维护着其元素的有序状态,无论是添加还是删除元素,都保证元素处于排序状态。
主要特点
-
元素排序: 在
TreeSet
中,元素按自然排序或者根据构造时提供的Comparator
进行排序。 -
唯一性: 与
HashSet
一样,TreeSet
不允许存储重复元素。 -
性能: 添加、删除和查找元素的时间复杂度为 O(log n)。
-
范围查找操作: 提供了丰富的方法来对有序集合进行操作,如
first()
,last()
,headSet()
,tailSet()
等。
主要方法
-
添加元素:
add(E e)
方法用于向TreeSet
添加新元素。 -
删除元素:
remove(Object o)
方法用于从TreeSet
中删除指定元素。 -
查找元素:
contains(Object o)
方法用于检查TreeSet
是否包含特定元素。 -
迭代元素: 可以使用迭代器或增强的 for 循环按排序顺序遍历
TreeSet
。 -
首尾元素:
first()
和last()
方法分别返回集合中最小和最大的元素。 -
子集操作: 如
headSet(toElement)
,tailSet(fromElement)
和subSet(fromElement, toElement)
方法用于获取TreeSet
的子集。
示例代码
import java.util.TreeSet;public class TreeSetExample {public static void main(String[] args) {TreeSet<String> treeSet = new TreeSet<>();treeSet.add("Banana");treeSet.add("Apple");treeSet.add("Cherry");System.out.println("TreeSet: " + treeSet);// 获取并输出第一个(最小的)元素String first = treeSet.first();System.out.println("First Element: " + first);// 获取并输出最后一个(最大的)元素String last = treeSet.last();System.out.println("Last Element: " + last);// 删除元素treeSet.remove("Apple");System.out.println("TreeSet after removal: " + treeSet);}
}
使用注意事项
-
元素排序:
TreeSet
要求存储的元素必须实现Comparable
接口,或者在创建TreeSet
时提供一个Comparator
。 -
空值处理: 如果使用自然排序,
TreeSet
不能包含null
元素。如果使用自定义比较器(Comparator
),则该比较器的实现决定是否可以包含null
。 -
性能考虑: 对于需要频繁进行添加、删除和包含操作的大量元素的场景,
TreeSet
的性能可能低于HashSet
。
TreeSet
由于其元素的有序性,适用于需要有序集合的场景,例如实现排行榜、范围查找或者维护一个按特定顺序排序的唯一元素集合。
3.3 Map
3.3.1 HashMap
HashMap
是 Java 中一种非常常用的集合类,它实现了 Map
接口。基于哈希表的实现,HashMap
存储键值对(Key-Value)映射,提供了快速的查找、插入和删除操作。
主要特点
-
键值对存储:
HashMap
存储的是键(Key)和值(Value)的映射。 -
键的唯一性: 每个键在
HashMap
中必须是唯一的。 -
值的可重复性: 不同的键可以映射到相同的值。
-
无序集合:
HashMap
中的元素没有特定的顺序。 -
空值和空键:
HashMap
允许存储一个null
键和多个null
值。 -
性能: 提供了常数时间复杂度的基本操作,如获取和插入,前提是哈希函数将键均匀分布在桶中。
主要方法
-
插入元素:
put(Key k, Value v)
方法用于向HashMap
中添加键值对。 -
获取元素:
get(Object key)
方法用于根据键获取对应的值。 -
删除元素:
remove(Object key)
方法用于删除指定键的键值对。 -
检查键存在:
containsKey(Object key)
方法用于检查HashMap
中是否包含指定的键。 -
遍历映射: 可以通过
keySet()
,values()
, 和entrySet()
方法来遍历HashMap
中的键、值或键值对。
示例代码
import java.util.HashMap;
import java.util.Map;public class HashMapExample {public static void main(String[] args) {Map<String, Integer> map = new HashMap<>();// 添加键值对map.put("Apple", 10);map.put("Banana", 20);map.put("Cherry", 30);// 访问值int value = map.get("Apple");System.out.println("Value for 'Apple': " + value);// 遍历映射for (Map.Entry<String, Integer> entry : map.entrySet()) {System.out.println(entry.getKey() + ": " + entry.getValue());}// 删除元素map.remove("Banana");System.out.println("Map after removal: " + map);}
}
使用注意事项
-
哈希冲突: 当不同的键有相同的哈希值时,会发生哈希冲突,这可能导致访问和插入操作的时间复杂度增加。
-
键的不可变性: 作为键的对象应该是不可变的,以保证哈希值的一致性。例如,常用的键类型有
String
和各种包装类型(如Integer
、Long
等)。 -
线程安全:
HashMap
不是线程安全的。在多线程环境中,可以考虑使用ConcurrentHashMap
或外部同步机制。
HashMap
由于其出色的平均性能表现,是实现映射的首选。它在处理大量数据时非常有效,特别是当你需要快速地查找、更新或删除键值对时。
HashMap的put()原理
HashMap
在 Java 中是基于哈希表的 Map 实现,它提供了快速的插入、查找和删除操作。让我们详细了解 HashMap
的插入元素的过程:
-
哈希函数
当向
HashMap
中插入一个键值对时,首先会使用哈希函数处理键对象,以确定该键值对应存储在哈希表的哪个位置(也称为桶)。- 计算哈希码:
HashMap
使用key.hashCode()
方法来计算键对象的哈希码。 - 哈希函数: 为了减少哈希冲突,
HashMap
对哈希码进行哈希函数处理,确定最终的桶索引。这通常涉及到将哈希码与数组大小相关联。
- 计算哈希码:
-
处理哈希冲突
由于哈希表的大小有限,不同的键可能会映射到同一个桶(哈希冲突)。
HashMap
使用链表(在 Java 8 及以后版本中,当链表长度超过一定阈值时,使用红黑树)来处理冲突。- 链地址法: 如果已经有一个或多个键值对存储在计算出的桶索引位置,则新的键值对会被存储在该位置的链表(或红黑树)中。
-
插入元素
- 新键值对: 如果桶索引位置为空,新的键值对作为第一个元素插入;如果位置不为空,则按照链表或红黑树的方式添加到该位置。
- 键的唯一性: 如果插入的键在
HashMap
中已存在,则新的值将覆盖旧值。
-
扩容
- 负载因子和容量:
HashMap
有一个负载因子(默认为 0.75),它是容量和当前键值对数量的比率。当HashMap
中的元素数量超过容量与负载因子的乘积时,哈希表将被扩容(通常是翻倍)。 - 重新哈希: 扩容后,现有的键值对需要根据新的数组大小重新计算哈希并重新分配到新的桶中。
- 负载因子和容量:
-
插入过程的注意事项
- 键的不变性: 键对象一旦用作
HashMap
的键,就不应该修改。因为任何对键对象的改变都可能影响其哈希码,从而使得无法在HashMap
中正确定位该键。 null
值处理:HashMap
允许键和值为null
。键为null
的元素总是映射到哈希表的第一个位置。
总的来说,
HashMap
的插入过程涉及计算哈希码、处理哈希冲突、可能的扩容和重新哈希。这个过程优化了速度和内存使用,使HashMap
成为在大多数情况下处理键值对映射的高效选择。 - 键的不变性: 键对象一旦用作
3.3.2 HashTable
Hashtable
是 Java 中的一种基本的集合类,它实现了 Map
接口。Hashtable
与 HashMap
类似,都提供了基于键的存储和快速检索的能力。然而,两者之间存在一些重要的差异。
主要特点
-
同步性:
Hashtable
是同步的。这意味着它是线程安全的,多个线程可以同时访问Hashtable
而不会引起并发问题。但这也意味着相对于非同步的HashMap
,Hashtable
在性能上可能会有所下降。 -
不允许
null
键或值: 与HashMap
不同,Hashtable
不允许使用null
作为键或值。 -
遗留类:
Hashtable
是 Java 早期版本的一部分(自 Java 1.0 起)。随着 Java 集合框架的引入,HashMap
成为了更加现代化的选择,提供了类似的功能但更高的性能。
主要方法
-
添加元素:
put(Key k, Value v)
方法用于向Hashtable
中添加键值对。 -
获取元素:
get(Object key)
方法用于根据键获取对应的值。 -
删除元素:
remove(Object key)
方法用于删除指定键的键值对。 -
遍历映射: 可以通过
keySet()
,values()
, 和entrySet()
方法来遍历Hashtable
中的键、值或键值对。
示例代码
import java.util.Hashtable;public class HashtableExample {public static void main(String[] args) {Hashtable<String, Integer> table = new Hashtable<>();table.put("Apple", 40);table.put("Banana", 10);table.put("Cherry", 30);// 获取键对应的值int value = table.get("Apple");System.out.println("Value for 'Apple': " + value);// 删除键值对table.remove("Banana");System.out.println("Hashtable after removal: " + table);}
}
使用注意事项
-
线程安全: 尽管
Hashtable
是线程安全的,但在多线程环境中,更推荐使用ConcurrentHashMap
,因为它提供了更高的并发性。 -
性能考虑: 由于
Hashtable
的所有公共方法都是同步的,这可能会导致在高负载时的性能问题。在单线程应用或不需要同步的场景下,HashMap
通常是更好的选择。 -
遗留类: 考虑到
Hashtable
是较早的 Java 集合类,新的代码应更倾向于使用HashMap
或ConcurrentHashMap
。
总的来说,虽然 Hashtable
在历史上是 Java 集合框架的重要组成部分,但现在通常推荐使用更现代的 HashMap
或 ConcurrentHashMap
,除非你需要一个线程安全的实现且无法使用 ConcurrentHashMap
。
3.3.3 ConcurrentHashMap
ConcurrentHashMap
是 Java 中的一个线程安全的 Map
实现,它是专门为高并发场景设计的。ConcurrentHashMap
在 Java 5 中引入,作为替代旧的线程安全类 Hashtable
和同步的 Collections.synchronizedMap
的一种更高效的方案。
主要特点
-
线程安全:
ConcurrentHashMap
通过使用分段锁(在 Java 8 中改进为使用 CAS 操作、同步块和内部锁)提供线程安全,这意味着多个线程可以同时安全地访问ConcurrentHashMap
实例。 -
高并发性能: 相比于
Hashtable
和Collections.synchronizedMap
,ConcurrentHashMap
提供了更好的并发性能。它在内部对数据进行分段,每个段可以独立加锁,从而允许多个线程并发地读写。 -
无锁读取:
ConcurrentHashMap
的读操作一般不需要加锁,因此读取操作非常快。 -
弱一致性迭代器: 迭代器具有弱一致性,而不是快速失败(fail-fast)。
主要方法
ConcurrentHashMap
实现了 Map
接口,因此它提供了类似于其他 Map
实现的方法,例如 put
, get
, remove
, containsKey
等。
示例代码
import java.util.concurrent.ConcurrentHashMap;public class ConcurrentHashMapExample {public static void main(String[] args) {ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();map.put("Key1", 10);map.put("Key2", 20);// 获取值int value = map.get("Key1");System.out.println("Value for Key1: " + value);// 替换值map.replace("Key1", 15);System.out.println("Updated Value for Key1: " + map.get("Key1"));// 迭代映射for (String key : map.keySet()) {System.out.println(key + ": " + map.get(key));}}
}
使用注意事项
-
并发修改: 在迭代过程中,
ConcurrentHashMap
允许插入和删除操作,迭代器反映了映射的状态,可能不反映所有最近的修改。 -
null
值和键:ConcurrentHashMap
不允许使用null
作为键或值。 -
大小估计:
size
方法提供的映射大小是一个近似值,因为映射可能在计算大小时发生更改。 -
性能考虑: 虽然
ConcurrentHashMap
提供了优异的并发性能,但在不需要高并发的场景中,使用普通的HashMap
可能更为高效。
ConcurrentHashMap
是处理高并发应用程序中的映射需求的理想选择,尤其是在多个线程需要频繁读写映射时。通过允许同时读取和写入操作,ConcurrentHashMap
显著提高了性能,同时保持了线程安全。