OOD:代表“面向对象设计”(Object-Oriented Design)是一种编程设计方法学,基于面向对象编程(OOP)的概念和原则,如封装、继承和多态。OOD的核心在于使用对象(具有属性和行为的实体)来模拟真实世界中的事物和交互。
OOP:代表面向对象编程(Object-Oriented Programming)是一种编程范式,它使用“对象”来设计软件。在OOP中,对象是包含数据和方法的实体,用于模拟现实世界中的事物和概念。面向对象编程的主要特点包括封装、继承和多态。
封装(Encapsulation)
封装是将数据(属性)和操作这些数据的代码(方法)捆绑在一起的过程。这样做的目的是隐藏对象的内部细节和实现,只暴露必要的操作接口。这有助于降低代码复杂性,提高安全性。
例子: 想象一下一个咖啡机。你不需要知道它内部是如何工作的,你只需要知道如何操作它(比如按下按钮来制作咖啡)。咖啡机的内部机制被封装起来,而用户界面提供了与之交互的方法。
继承(Inheritance)
继承是一种允许新创建的对象继承现有对象的属性和方法的机制。这有助于减少代码重复,增强代码的可重用性。
例子: 假设我们有一个“动物”类,包含所有动物共有的属性和方法,如“呼吸”和“移动”。我们可以创建一个“狗”类,继承自“动物”类。这样,“狗”类就自动拥有了“呼吸”和“移动”的能力,而我们也可以为“狗”添加专有的属性和方法,如“汪汪叫”。
可以使用组合关系代替继承关系。
多态(Polymorphism)
多态是指允许不同类的对象对同一消息做出响应的能力,即同一个接口可以被不同的对象以不同的方式实现。一个对象在运行时的多种形态。(接口与实现类)
例子: 继续上面的“动物”类的例子,假设有一个方法叫“发出声音”。不同的动物发出的声音是不同的,狗会“汪汪叫”,猫会“喵喵叫”。多态允许我们对不同的动物对象调用同一个“发出声音”的方法,但每个动物会以它自己的方式来响应。
结合例子理解面向对象思想
想象你正在开发一个模拟动物园的软件。你创建了一个基类“动物”,它定义了所有动物共有的属性和行为,如年龄、体重和“移动”的方法。然后你创建了几个继承自“动物”的子类,如“狮子”、“大象”和“长颈鹿”,每个子类有其特有的属性和行为。例如,“狮子”类可能有一个“吼叫”的方法,而“长颈鹿”类可能有一个“吃树叶”的方法。这里,封装使得每个类的实现细节对外部是隐藏的,继承让你可以重用“动物”类的代码,而多态允许你以统一的方式处理不同类型的动物。
面向对象编程的这些特性使得代码更易于理解、维护和扩展。通过模拟现实世界的实体和概念,它允许开发者以更自然的方式思考和解决问题。
重载(Overloading)和重写(Overriding)是面向对象编程中两个基本且重要的概念,尽管它们听起来相似,但它们在功能和用途上有显著的区别。
重载是指在同一个类中有多个同名方法,但这些方法的参数列表不同(参数的类型、个数或顺序不同)。
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
方法有相同的名称,但参数列表不同。
重写是指子类重新定义父类中的某个方法的实现。子类的方法必须和父类被重写的方法具有相同的方法名称、返回类型和参数列表。
class Animal {
public void makeSound() {
System.out.println("Some sound");
}
}
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Bark");
}
}
class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("Meow");
}
}
在这个例子中,Dog
和 Cat
类重写了它们从 Animal
类继承的 makeSound
方法。
重载使得同一个方法可以根据不同的参数执行不同的功能,而重写则是用于实现多态,即同一个接口的不同实现。
在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); // 调用父类的构造器
}
@Override
public 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
引用父类的成员。这在处理继承时特别有用,可以帮助区分子类和父类中同名的属性和方法。
继承和实现都是实现代码重用和功能扩展的重要手段。在Java中,继承是单继承,即每个类只能继承一个父类,而实现则是多重的,即一个类可以实现多个接口。这两种机制共同工作,为Java编程提供了强大的灵活性和表达力。
Java Bean
类必须public 修饰
必须有一个无参构造器
所有字段私有化且提供getter/setter方法
扩展:
字段:类中的变量称之为字段
属性:类中的getter/setter只要存在之一,getXxx中的xxx就是属性名称
内省机制
Java提供的一套更便于操作Java Bean属性的API(相较于反射更容易操作Java Bean属性的API)。
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!"
。这些方法是处理字符串时非常常用的操作,能够满足大多数基本的字符串处理需求。
在Java中,Comparable
和 Comparator
接口都用于实现对象的排序,但它们在用法和目的上有一些关键的区别。
Comparable
接口用于定义对象自然排序的方式。类通过实现 Comparable
接口的 compareTo
方法来定义其对象的排序逻辑。
Comparable
接口,它的对象集合可以直接使用 Collections.sort
或 Arrays.sort
进行排序。public class Person implements Comparable<Person> {
private int age;
public Person(int age) {
this.age = age;
}
@Override
public int compareTo(Person other) {
return this.age - other.age; // 年龄的升序排序
}
}
在这个例子中,Person
类实现了 Comparable
接口,以年龄进行自然排序。
Comparator
接口用于定义一个外部的排序逻辑,它允许定义多种排序规则,或者在类没有实现 Comparable
接口的情况下提供排序逻辑。
Comparator
实现类来定义不同的排序规则。public class AgeComparator implements Comparator<Person> {
@Override
public int compare(Person p1, Person p2) {
return p1.getAge() - p2.getAge(); // 年龄的升序排序
}
}
在这个例子中,AgeComparator
提供了 Person
对象按年龄排序的规则。
实现位置:
Comparable
是在类的内部实现的,定义了对象的自然排序顺序。Comparator
是在类的外部实现的,定义了一种外部的排序规则。方法数量:
Comparable
只包含一个方法 compareTo
。Comparator
包含一个方法 compare
。控制权:
Comparable
时,类的设计者控制着其排序逻辑。Comparator
时,类的用户可以定义自己的排序逻辑。灵活性:
Comparable
提供单一的自然排序,不够灵活。Comparator
可以提供多种排序逻辑,更加灵活。根据不同的需求选择使用 Comparable
或 Comparator
。如果一个类有一个明确的、自然的排序逻辑(如数字、字母顺序),则使用 Comparable
;如果需要多种排序方式,或者类本身不具备自然排序逻辑,或者你无法修改类的源代码,那么使用 Comparator
是更好的选择。
双检加锁机制
if(){ // 第一次检查
synchronized(obj){ // 检查
if(){ // 第二次检查
}
}
}
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 中使用最广泛的集合之一。它是实现列表功能的优选,特别是当需要频繁访问列表中的元素时。
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
可能是一个更好的选择。
ArrayList
和 LinkedList
都是 Java 中的 List
接口的实现,但它们在内部数据结构和性能特性上有显著的不同。了解这些差异有助于选择最适合特定场景的数据结构。
ArrayList
基于动态数组实现,适用于频繁的读取操作。
随机访问快: ArrayList
支持快速的随机访问,因为它允许直接通过索引访问元素(时间复杂度为 O(1))。
修改慢: 在列表中间插入或删除元素比较慢,因为这可能涉及移动元素以维护数组的连续性(时间复杂度为 O(n))。
内存占用: 相对较高的内存开销,因为它在数组的基础上维护容量(capacity),且扩容操作涉及复制元素到新的数组。
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
更适合于插入和删除操作频繁的场景。正确选择两者之一可以显著提高程序的性能和效率。
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
由于其在处理大量数据时的高效性和简便性,是实现集合操作的首选,特别是当不需要元素排序或重复元素时。
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
由于其元素的有序性,适用于需要有序集合的场景,例如实现排行榜、范围查找或者维护一个按特定顺序排序的唯一元素集合。
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
在 Java 中是基于哈希表的 Map 实现,它提供了快速的插入、查找和删除操作。让我们详细了解 HashMap
的插入元素的过程:
哈希函数
当向 HashMap
中插入一个键值对时,首先会使用哈希函数处理键对象,以确定该键值对应存储在哈希表的哪个位置(也称为桶)。
HashMap
使用 key.hashCode()
方法来计算键对象的哈希码。HashMap
对哈希码进行哈希函数处理,确定最终的桶索引。这通常涉及到将哈希码与数组大小相关联。处理哈希冲突
由于哈希表的大小有限,不同的键可能会映射到同一个桶(哈希冲突)。HashMap
使用链表(在 Java 8 及以后版本中,当链表长度超过一定阈值时,使用红黑树)来处理冲突。
插入元素
HashMap
中已存在,则新的值将覆盖旧值。扩容
HashMap
有一个负载因子(默认为 0.75),它是容量和当前键值对数量的比率。当 HashMap
中的元素数量超过容量与负载因子的乘积时,哈希表将被扩容(通常是翻倍)。插入过程的注意事项
HashMap
的键,就不应该修改。因为任何对键对象的改变都可能影响其哈希码,从而使得无法在 HashMap
中正确定位该键。null
值处理: HashMap
允许键和值为 null
。键为 null
的元素总是映射到哈希表的第一个位置。总的来说,HashMap
的插入过程涉及计算哈希码、处理哈希冲突、可能的扩容和重新哈希。这个过程优化了速度和内存使用,使 HashMap
成为在大多数情况下处理键值对映射的高效选择。
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
。
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
显著提高了性能,同时保持了线程安全。