主题
JavaSE
什么是JavaSE
JavaSE,全称为Java Standard Edition,是Java平台的标准版,由Oracle公司(先前由Sun Microsystems公司)开发和维护,构成了Java技术的核心与基础部分。JavaSE主要包含以下几个关键组成部分:
Java编程语言
定义了完整的语法、类型系统和面向对象编程模型的标准规范。
Java虚拟机(JVM)
跨平台的抽象计算机系统,负责编译并执行Java源代码编译后的字节码。
Java类库(Java API)
- 提供了一系列广泛的预定义软件包,如:
java.lang、java.util、java.io、java.net等,分别提供了基础类、集合框架、I/O操作、网络通信等功能。- 同时包括构建图形用户界面(GUI)的库如Swing和JavaFX。
- 还涵盖了安全性、多线程支持以及用于Java Web服务开发的支持类。
JavaSE主要用于
- 开发和部署桌面应用程序
- 服务器端应用程序
- 嵌入式系统应用程序
- 实时环境下的应用程序
作为Java技术体系的基石,JavaSE为开发Java ME(Micro Edition,针对移动和嵌入式设备)和Java EE(Enterprise Edition,针对企业级应用)提供了必需的基础平台。
借助JavaSE,开发者能够基于统一标准开发适用于多种操作系统的应用程序,充分利用Java的“Write Once, Run Anywhere”(一次编写,到处运行)特性。尽管随着时间推移,Java Applet在现代浏览器中的应用逐渐减少,但JavaSE依然是构建各种类型Java应用的关键平台。
Java程序设计(基础)- 数据类型
数据类型,分为两大类型:
- 基本数据类型(普通的值)
- 数值类型
- 整数类型(byte、short、int、long)
- 浮点类型(float、double)
- 字符型(char)
- 布尔型(boolean)
- 数值类型
- 引用数据类型(传递的内存地址)
- 类(class)
- 接口(interface)
- 数组
Java基本数据类型
Java编程语言提供了八种基本数据类型,它们可以被划分为以下四类:
1. 整数类型:
byte: 占用1字节,有符号整数,取值范围从-128到127。javabyte smallNum = 0; // 默认值 byte smallNum = 127;short: 占用2字节,有符号整数,取值范围从-32,768到32,767。javashort notSoSmall = 0; // 默认值 short notSoSmall = 32767;int: 占用4字节,有符号整数,默认整数类型,取值范围从-2^31到2^31 - 1。javaint normalInt = 0; // 默认值 int normalInt = 2_147_483_647; // 注意Java允许下划线进行数字分隔以提高可读性long: 占用8字节,有符号整数,需要在数值后添加L或l标识符,取值范围从-2^63到2^63 - 1。javalong largeNumber = 0L; // 默认值 long largeNumber = 9_223_372_036_854_775_807L;
2. 浮点数类型:
float: 占用4字节,单精度浮点数,取值范围较大但精度相对较低,常量后面需跟F或f标识符。javafloat decimalValue = 0.0f; // 默认值 float decimalValue = 3.1415926535f;double: 占用8字节,双精度浮点数,这是Java默认的浮点数类型,具有更大的精度范围。javadouble preciseDecimal = 0.0; // 默认值 double preciseDecimal = 3.141592653589793;
3. 字符类型:
char: 占用2字节,无符号Unicode字符,通常用单引号括起来。javachar letterA = ''; // 默认值 \u0000(空,'') char letterA = 'A';
4. 布尔类型:
boolean: 表示逻辑值,仅能取两个值true或false。javaboolean flag = false; // 默认值 boolean flag = true;
此外,每个基本类型都有对应的包装类,例如 Byte, Short, Integer, Long, Float, Double, Character, 和 Boolean。基本类型与其对应的包装类型之间的赋值使用自动装箱与拆箱完成。
java
Integer x = 1; // 装箱
int y = x; // 拆箱对于数据来说,设置内容的时候应该放在整个允许的范围之中。如果超过了此范围,肯定会出现数值不正确的情况。 故在实际编程中,根据需要准确选择合适的数据类型是非常重要的。
缓存池
在 Java 中,new Integer(0) 和 Integer.valueOf(0) 都用于创建代表整数值 0 的 Integer 对象,但是它们之间有一个重要的性能和内存使用的区别:
new Integer(0):
- 这是通过调用
Integer类的构造函数显式地创建一个新的Integer对象。 - 每次执行
new Integer(0),都会在堆内存中创建一个新的对象实例。
javaInteger a = new Integer(0); Integer b = new Integer(0); // a 和 b 是不同的对象,指向不同的内存地址 System.out.println(a == b); // 输出: false- 这是通过调用
Integer.valueOf(0):
- 这是一个静态工厂方法,它会尝试从一个缓存池中获取对应整数值的对象引用。
- 对于
-128到127这个范围内的整数值,Integer.valueOf()方法会重用已经存在的对象,而不是每次都创建新的对象。 - 因此,当连续两次调用
Integer.valueOf(0)时,如果值在缓存范围内,实际上会返回相同的对象引用。
javaInteger c = Integer.valueOf(0); Integer d = Integer.valueOf(0); // 如果是在缓存范围内(如0在这个范围内),c 和 d 指向的是同一个对象 System.out.println(c == d); // 输出: true
总结起来,在处理小整数值时,Integer.valueOf() 能够提高效率并减少内存占用,因为它可能避免了不必要的对象创建。 而对于超出缓存范围的整数值,Integer.valueOf() 表现得与 new Integer() 相似,会创建新的对象实例。
提示
在 JDK 1.8 所有的数值类缓冲池中,Integer 的缓冲池 IntegerCache 很特殊,这个缓冲池的下界是 -128,上界默认是 127, 但是这个上界是可调的,在启动 JVM 的时候,通过 -XX:AutoBoxCacheMax=<size> 来指定这个缓冲池的大小, 该选项在 JVM 初始化的时候会设定一个名为 java.lang.IntegerCache.high 系统属性,然后 IntegerCache 初始化的时候就会读取该系统属性来决定上界。
数据类型的转换
- 自动类型转换(条件皆成立)
- 转换前的数据类型与转换后的类型兼容。
- 转换后的数据类型表示范围比转换前的类型大。
- 强制类型转换
- 整数运算,结果也会是整数。
提示
任何类型的数据都向String转型。
Java程序设计(基础)- 运算符与表达式
运算符与表达式
在Java程序设计中,运算符和表达式是构建程序逻辑的基础之一。它们帮助我们执行计算、比较值、改变变量状态等操作。下面简要介绍Java中一些基本的运算符与表达式概念。
1. 算术运算符
- 加法 (+): 相加两个数值。
- 减法 (-): 从一个数值中减去另一个数值。
- 乘法 (*): 两个数值相乘。
- 除法 (/): 除以一个数得到商。注意整数除法会向下取整。
- 取模 (%): 得到除法的余数。
- 自增 (++) 和 自减 (--): 分为前缀和后缀形式,用于增加或减少变量的值。
2. 关系运算符
用于比较两个值,返回布尔值(true或false)。
- 等于 (==)
- 不等于 (!=)
- 大于 (>)
- 小于 (<)
- 大于等于 (>=)
- 小于等于 (<=)
3. 逻辑运算符
用于连接或反转布尔表达式。
- 逻辑与 (&&): 如果两边的操作数都为true,则结果为true。
- 逻辑或(||): 如果两边的操作数至少有一个为true,则结果为true。
- 逻辑非 (!): 反转操作数的布尔值。
4. 位运算符
对二进制位进行操作。
- 按位与 (&): 对应位都是1时结果才为1。
- 按位或 (|): 对应位至少有一个为1时结果为1。
- 按位异或 (^): 对应位不同为1,相同为0。
- 按位取反 (~): 对操作数的每一位取反。
- 左移 (<<): 将操作数的二进制表示向左移动指定位数,右边补0。
- 右移 (>>): 向右移动指定位数,正数补0,负数补1(有符号右移);无符号右移(>>>)总是补0。
5. 赋值运算符
用于给变量赋值。
- 简单赋值 (=)
- 复合赋值运算符: 如
+=,-=,*=,/=,%=,&=,|=,^=和>>=等,结合了赋值和特定的算术/位运算。
6. 条件运算符(三元运算符)
形式为 条件 ? 表达式1 : 表达式2,如果条件为true,则结果为表达式1,否则为表达式2。
表达式
表达式是由变量、常量、运算符和方法调用等组成的,能够求得一个值的组合。例如,a + b 是一个简单的算术表达式,x > 5 && y < 10 是一个逻辑表达式。
掌握这些基本运算符和表达式的使用,对于编写有效的Java代码至关重要。实践中,结合具体问题灵活运用它们,可以构建出复杂的逻辑和算法。
Java面向对象(基础)- 三大特性
一、面向对象概述
面向对象编程(Object-Oriented Programming,OOP)是一种流行的编程范式,它提倡将程序设计组织为相互作用的对象集合,每个对象都包含了数据(属性或状态)以及处理数据的方法(行为)。在Java中,面向对象的主要特点包括:
1. 封装 (Encapsulation):
封装是将数据和操作数据的方法捆绑在一起,并对外隐藏内部细节,仅通过公开接口访问数据或执行操作。例如:
java
public class Car {
// 封装数据字段(私有)
private String brand;
private int year;
// 提供公共方法来设置和获取数据
public void setBrand(String brand) {
this.brand = brand;
}
public String getBrand() {
return brand;
}
public void setYear(int year) {
this.year = year;
}
public int getYear() {
return year;
}2. 继承 (Inheritance):
继承允许一个类(子类)从另一个类(父类)继承特性并扩展它们。子类自动拥有父类的所有非私有属性和方法,还可以定义自己的特性和方法。
java
public class SportsCar extends Car {
private boolean convertible;
public SportsCar(String brand, int year, boolean isConvertible) {
super(brand, year); // 调用父类构造器
this.convertible = isConvertible;
}
public boolean isConvertible() {
return convertible;
}
}3. 多态 (Polymorphism):
多态意味着一个接口可以有多种形态,即同一消息可以根据发送给的不同对象产生不同的行为。Java中主要是通过接口实现和方法重写来体现多态性。
java
public interface Driveable {
void drive();
}
public class Car implements Driveable {
// 实现Driveable接口
@Override
public void drive() {
System.out.println("Driving the car...");
}
}
public class Bicycle implements Driveable {
// 实现Driveable接口
@Override
public void drive() {
System.out.println("Riding the bicycle...");
}
}
public static void main(String[] args) {
List<Driveable> vehicles = new ArrayList<>();
vehicles.add(new Car());
vehicles.add(new Bicycle());
for (Driveable vehicle : vehicles) {
vehicle.drive(); // 显示多态行为
}
}二、类与对象
- 类 (Class) 是Java中定义对象蓝图的模板。它描述了一组相关属性和方法。
- 对象 (Object) 是类的一个实例,具有类中定义的状态和行为。
- 实例化 (Instantiation) 创建对象的过程,如
Car myCar = new Car(); - 构造方法 (Constructor) 用于初始化对象,具有与类同名且无返回类型的特殊方法。
java
public class Car {
// 构造方法
public Car(String brand, int year) {
this.brand = brand;
this.year = year;
}
}三、静态成员
静态域 (Static Fields) 在类级别上定义,所有类实例共享同一个静态变量。
javapublic class Car { public static int totalCars = 0; // ... } // 使用 Car.totalCars++;静态方法 (Static Methods) 可以直接通过类调用,无需实例化对象,不能访问非静态成员。
javapublic class Car { public static void increaseTotalCars() { Car.totalCars++; } } // 使用 Car.increaseTotalCars();
以上是Java面向对象编程的一些核心概念和示例代码,更多面向对象编程实践还会涉及到接口、抽象类、包、访问修饰符等其他重要概念。
Java面向对象(基础)- 三大特性 - 继承
- 访问权限
- 抽象类与接口
- 重写与重载
super关键词
Java面向对象(基础)- String类
String类
1. Java中的String类
在Java编程语言中,String 类代表不可变的字符序列。它是Java核心API中的一个关键类,位于 java.lang 包下,因此不需要显式导入即可使用。 String 类提供的字符串对象一旦创建,其内容就不能更改。每次对字符串进行修改操作(如拼接、替换等),实际上都会生成一个新的 String 对象。
java
// 创建String对象
String str1 = "Hello";
String str2 = "World";
// 不可变性的示例
str1 += " World"; // 这将创建一个新的String对象而非修改原对象
System.out.println(str1); // 输出: Hello World
System.out.println(str2); // 输出: World,原始对象不受影响
// 字符串常量池引用
String str3 = "Hello";
String str4 = "Hello";
System.out.println(str3 == str4); // 输出: true,两个引用指向了相同的字符串常量池对象2. 字符串常量池与intern()方法
Java虚拟机(JVM)为了提高性能和减少内存开销,维护了一个称为“字符串常量池”的区域。当创建一个字符串字面量时(如上述代码中的 "Hello"),JVM会在字符串常量池中检查是否存在相同内容的字符串对象。如果存在,则直接引用池中的对象;如果不存在,则创建新的字符串对象并放入池中。
java
String str5 = new String("Hello");
String str6 = "Hello";
System.out.println(str5 == str6); // 输出: false,因为str5是new出来的对象,而str6直接引用了常量池中的对象
str5 = str5.intern(); // 调用intern方法后,str5尝试将其引用改为指向常量池中的对象
System.out.println(str5 == str6); // 输出: true,现在str5和str6都指向了常量池中的同一对象3. HotSpot JVM中字符串常量池存放
在Oracle的HotSpot JVM实现中,字符串常量池在JDK 6及更早版本位于永久代(PermGen)空间中,而在JDK 7及更高版本中,字符串常量池被移到了堆内存中。具体来说,字符串常量池被存储在一个名为StringTable的哈希表结构中,这是一个全局的数据结构,这意味着在单个JVM实例中,所有线程和类共享同一个字符串常量池。
提示
Java中的String类作为不可变对象,在HotSpot JVM中有特殊的优化处理。通过利用字符串常量池,JVM能够有效地复用相同的字符串实例,从而节省内存并提升程序运行效率。同时,intern()方法提供了一种主动将字符串对象放入常量池的手段,使得具有相同内容的字符串能够确保引用相等,这对于内存管理以及一些特定场景下的性能优化至关重要。
Java面向对象(高级)- 集合
Java 类集
类集实际上就是一个动态的对象数组,与一般的对象数组不同,类集中的对象内容可以任意扩充。
- Map
- HashMap
- HashTable
- TreeMap
- LinkedHashMap
- Collection
- List
- Vector
- ArrayList
- LinkedList
- Set
- HashSet
- TreeSet
- LinkedHashSet
- Queue
- LinkedList
- PriorityQueue
- List
Map - HashMap
基于哈希表实现的无序键值对集合。
主要特点:
- 高效性:通过哈希表进行数据存储,可以实现近乎常数时间复杂度 O(1) 的插入、删除和查找操作(理想情况下)。
- 存储键值对:HashMap 可以存储任意类型的键值对,其中键 (Key) 和值 (Value) 都可以是任何对象类型。
- 无序:HashMap 中的元素没有顺序,添加顺序与取出顺序可能不一致。
创建和使用 HashMap 的基本示例:
java
import java.util.HashMap;
public class Main {
public static void main(String[] args) {
// 创建一个 HashMap 实例
HashMap<String, Integer> map = new HashMap<>();
// 添加键值对
map.put("Apple", 1);
map.put("Banana", 2);
map.put("Cherry", 3);
// 获取值
int appleCount = map.get("Apple"); // 输出:1
// 判断是否存在某个键
boolean containsKey = map.containsKey("Banana"); // 输出:true
// 删除键值对
map.remove("Cherry");
// 遍历 HashMap
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
}
}
}注意
- HashMap 在高并发环境下可能会导致数据不一致的问题,此时应考虑使用
ConcurrentHashMap。 - 如果映射的 Key 不重写 equals() 和 hashCode() 方法,或者方法实现得不好,可能导致无法正常存取或数据丢失等问题。因此,作为 HashMap 的 Key 的类需要正确且一致地实现这两个方法。
HashMap 在不同的JDK版本中有过显著的变化,以下是关于JDK 1.7和1.8版本之间实现差异的概述:
JDK 1.7中的HashMap实现:
- 结构:HashMap由一个数组和链表组成,每个数组桶(bucket)存储一个链表,当发生哈希冲突时(即多个键映射到同一个数组索引),这些键值对将被链接在这个链表中。
- 扩容机制:当HashMap中的元素数量超过阈值(threshold)时,会触发扩容操作,扩容后的新容量通常是原容量的两倍加一。
- 链表处理:对于哈希冲突,采用链地址法解决,也就是拉链法。插入新元素时,如果桶内已经有元素,就将新元素加入链表头部或尾部(取决于JDK版本,1.7之前是头插,1.7开始改为尾插,减少“逆序”的问题)。
- 性能:在某些情况下,如果链表很长,由于插入和查找都需要遍历链表,性能会退化到O(n)。
JDK 1.8中的HashMap改进:
- 结构:保留了数组+链表的基本结构,但在链表长度达到一定程度(默认8)后,链表会自动转换为红黑树(Red-Black Tree),从而优化了查找、插入和删除操作的性能,确保了最坏情况下的操作时间复杂度为O(log n)。
- 扩容机制:同样在元素数量超过阈值时进行扩容,但引入了一个新的概念“负载因子”(load factor),并且扩容后容量总是2的幂次方,这样可以利用位运算快速定位数组索引。
- 哈希算法:使用了一种称为“二次探测再散列”(double hashing)的技术来分散冲突,增加了高位参与计算索引位置,使得即使在扩容后,原有的键值对依然能够更均匀地分布到新的数组中。
- 初始化容量与预填充:提供了初始化时直接指定容量以及预填充因子的功能,以尽量避免多次扩容带来的性能损耗。
总结来说,JDK 1.8版本的HashMap相较于1.7版本,在处理大量哈希冲突时有了明显的性能提升,特别是在长期运行且存在大量冲突的情况下,通过红黑树的引入有效降低了操作的复杂度。同时,1.8版还对哈希算法进行了优化,提高了空间利用率和整体性能。
Map - HashTable
与 HashMap 类似,HashTable 也使用哈希表实现,但是有以下几点重要的不同之处:
- 线程安全性:
HashTable是线程安全的,这意味着在多线程环境下可以直接使用,而不需要外部同步控制。其内部的方法调用都会进行必要的锁操作来保证线程安全,但这也会导致其性能相比HashMap有所下降。 - 不允许 null 值:
HashTable不允许插入 null 键或 null 值,而HashMap允许键和值都为 null,但只能有一个 null 键。 - 设计年代较早:相比于
HashMap,HashTable是一个较早的设计,从 Java 1.0 就已存在,而HashMap出现在 Java 1.2 版本中。因此,HashTable并未使用 Java 5 引入的泛型。 - 迭代器:
HashTable返回的Enumeration在遍历时并不支持remove()操作,而HashMap返回的Iterator支持。
实践中,通常建议优先使用 ConcurrentHashMap(并发环境)或 HashMap(单线程环境)替代 HashTable,因为它们在性能和易用性上具有优势。只有在需要兼容老代码或特别关注线程安全但又不想使用 ConcurrentHashMap 的场景下,才可能会选择使用 HashTable。
Map - TreeMap
与 HashMap 和 HashTable 不同的是,TreeMap 使用红黑树(Red-Black Tree)作为底层数据结构。
主要特点:
- 排序特性:
TreeMap的键是根据键的自然顺序(如果键实现了Comparable接口)或自定义比较器(通过构造函数传递Comparator对象)进行排序的。因此,它始终按照排序顺序维护元素,并提供了如firstKey()、lastKey()、lowerKey()、higherKey()等用于获取排序范围内的键的方法。 - 访问性能:对于包含 n 个元素的
TreeMap,插入、删除和查找的时间复杂度大致为 O(log n),这是因为红黑树的平衡特性保证了查询效率。 - 键值对操作:除了常规的增删查改操作外,
TreeMap还支持按照键的顺序进行遍历。
java
import java.util.TreeMap;
public class Main {
public static void main(String[] args) {
TreeMap<String, Integer> treeMap = new TreeMap<>();
// 添加键值对
treeMap.put("Apple", 1);
treeMap.put("Banana", 2);
treeMap.put("Cherry", 3);
// 自然排序
System.out.println(treeMap); // 输出按字典顺序排序的键值对
// 定义自定义比较器
TreeMap<Integer, String> customSortedMap = new TreeMap<>(Collections.reverseOrder());
customSortedMap.put(3, "Three");
customSortedMap.put(1, "One");
customSortedMap.put(2, "Two");
// 按自定义顺序排序
System.out.println(customSortedMap); // 输出按降序排列的键值对
// 获取最小和最大键
System.out.println("First key: " + treeMap.firstKey());
System.out.println("Last key: " + treeMap.lastKey());
}
}Map - LinkedHashMap
继承自 HashMap 类,同时实现了 Map 接口。LinkedHashMap 继承了 HashMap 的所有特性,但增加了一个特性——维护插入顺序或访问顺序。
主要特点:
- 双向链表:
LinkedHashMap内部使用哈希表和双向链表相结合的方式来存储数据,因此它可以记住元素的插入顺序或最近访问顺序。- 插入顺序模式:默认情况下,
LinkedHashMap会按照插入顺序维护元素的顺序,即先插入的元素在迭代时会先出现。 - 访问顺序模式:可以通过设置构造函数参数
accessOrder为true来启用访问顺序模式。在这种模式下,每当访问(get 或 put)一个元素时,该元素就会移动到链表的末尾,因此迭代输出的顺序就是最近访问过的元素顺序。
- 插入顺序模式:默认情况下,
- 性能:由于额外维护了一个双向链表,所以相对于
HashMap,LinkedHashMap的插入、删除和查找操作可能会稍慢一些,但在大多数情况下,这种性能影响是可以接受的。
使用示例:
java
import java.util.LinkedHashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
// 插入顺序示例
LinkedHashMap<String, Integer> linkedHashMap = new LinkedHashMap<>();
linkedHashMap.put("Apple", 1);
linkedHashMap.put("Banana", 2);
linkedHashMap.put("Cherry", 3);
// 遍历输出,按插入顺序
for (Map.Entry<String, Integer> entry : linkedHashMap.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// 访问顺序示例
LinkedHashMap<String, Integer> accessOrderedMap = new LinkedHashMap<>(16, 0.75f, true);
accessOrderedMap.put("Apple", 1);
accessOrderedMap.put("Banana", 2);
accessOrderedMap.put("Cherry", 3);
// 访问元素,然后再次遍历输出,按访问顺序
System.out.println(accessOrderedMap.get("Banana"));
for (Map.Entry<String, Integer> entry : accessOrderedMap.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
}
}总结来说,LinkedHashMap 在需要维持元素插入顺序或访问顺序的应用场景下非常有用,比如缓存系统(LRU策略)等。
Collection - List - Vector
在 Java 中,java.util.Vector 是早期版本集合框架的一部分,也是基于数组实现的,它除了具备类似 ArrayList 的特性外,还自带线程安全机制,这意味着它在多线程环境下的操作会自动同步,但这也会带来一定的性能开销。从 Java 1.2 开始,推荐使用 ArrayList 代替 Vector,除非确实需要线程安全功能。
Collection - List - ArrayList
基于动态数组实现的,能够以对象引用的形式存储一系列元素,并且这些元素在 ArrayList 中是有序的,可以通过索引进行访问。
主要特点:
- 动态扩容:当向
ArrayList中添加元素,如果当前容量不足以容纳新元素时,ArrayList会自动扩展其内部数组的大小,通常是扩大为原来的 1.5 倍(默认策略)。 - 可变大小:
ArrayList的大小不是固定的,可以根据需要增加或减少元素的数量。 - 访问速度快:由于
ArrayList内部采用数组存储,因此通过索引访问元素的时间复杂度为 O(1),即常数时间访问。 - 插入和删除操作:在
ArrayList的中间位置插入或删除元素时,需要移动后续元素,所以插入和删除操作(非尾部)的时间复杂度一般为 O(n)。 - 允许重复元素:
ArrayList可以包含重复的元素,并且保持元素的插入顺序。 - 不是线程安全的:在多线程环境下,如果不采取额外的同步措施,直接对
ArrayList进行读写操作可能会导致数据不一致。若需要线程安全的 List,可以使用Collections.synchronizedList(new ArrayList<...>())方法将其包装成线程安全的 List。
使用示例:
java
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("Apple");
list.add("Banana");
list.add("Cherry");
System.out.println(list.get(0)); // 输出 "Apple"
list.remove(1); // 删除第二个元素 "Banana"
list.add(1, "Durian"); // 在索引1的位置插入"Durian"
System.out.println(list); // 输出 ["Apple", "Durian", "Cherry"]
}
}自动扩容:
- 初始容量:默认初始容量为10。
- 扩容条件:添加元素,超过
ArrayList的大小,就需要进行扩容。 - 扩容策略:数组扩容通过
ensureCapacity(int minCapacity)方法来实现。在实际添加大量元素前,我也可以使用ensureCapacity来手动增加ArrayList实例的容量。通常扩容当前容量翻倍(实际上大约增加50%,即原容量的1.5倍),该操作代价很高,实际使用时,应该尽量避免数组容量的扩张。
快速失败(Fail-Fast)机制:
Fail-Fast机制是Java集合(Collection)的一种错误传播机制,当多个线程对集合进行结构上的改变时,有可能会产生Race Condition(竞态条件),当出现这种情况时,ArrayList会尽快的抛出ConcurrentModificationException异常。
错误示例
java
public class FailFastExample {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (item.equals("B")) {
list.remove(item); // 错误方式,会引发Fail-Fast机制
}
}
}
}正确示例
java
public class FailFastExample {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (item.equals("B")) {
iterator.remove(); // 正确方式,使用Iterator的remove方法
}
}
}
}Collection - List - LinkedList
一种链表结构,由一系列节点(Node)组成,每个节点包含一个元素和指向下一个节点的引用。
主要特点:
- 结构特性:
LinkedList分为双向链表和单向链表两种形式。Java 中的LinkedList实现的是双向链表,每个节点都有前驱和后继节点的引用。相比于 ArrayList 使用连续的内存空间存储元素,LinkedList的元素在内存中分布不一定连续。 - 动态增长:与 ArrayList 类似,
LinkedList的大小也是可变的,可以在任意位置添加或删除元素。 - 访问速度:访问
LinkedList中的元素需要从头或尾节点开始沿着引用逐个查找,所以通过索引访问元素的时间复杂度为 O(n)。但在首尾节点处的添加和删除操作(addFirst(),addLast(),removeFirst(),removeLast())的时间复杂度为 O(1)。 - 插入和删除操作:在
LinkedList中插入或删除元素(不仅仅是首尾),不需要像 ArrayList 那样移动其他元素,所以在非首尾位置插入和删除元素的时间复杂度也仅为 O(1)。 - 允许重复元素:
LinkedList同样可以包含重复的元素,并且保持元素的插入顺序。 - 不是线程安全的:和
ArrayList一样,在多线程环境下,如果不采取额外的同步措施,直接对LinkedList进行读写操作可能会导致数据不一致。
使用示例:
java
import java.util.LinkedList;
public class Main {
public static void main(String[] args) {
LinkedList<String> list = new LinkedList<>();
list.add("Apple");
list.add("Banana");
list.add("Cherry");
System.out.println(list.peek()); // 输出 "Apple",查看但不移除第一个元素
list.removeFirst(); // 移除并返回第一个元素 "Apple"
list.add("Durian"); // 添加到末尾
list.add(1, "Elderberry"); // 在索引1的位置插入"Elderberry"
System.out.println(list); // 输出 ["Banana", "Elderberry", "Cherry", "Durian"]
}
}Collection - Set - HashSet
不允许存储重复元素,并且没有元素的顺序性保证。
主要特点:
- 存储唯一性:
HashSet使用哈希表(HashMap)的方式来存储元素,通过元素的 hashCode() 方法计算哈希值并确定存储位置,如果有两个元素的hashCode相同且equals方法也认为它们相等,则只存储其中一个。 - 无序性:
HashSet不保证元素的迭代顺序,每次遍历的结果可能不同,特别是当集合被修改后。 - 查找速度快:由于
HashSet采用了哈希算法,因此对于插入、删除和查找操作具有较好的性能,理想情况下,这些操作的时间复杂度接近 O(1)。 - 不是线程安全的:如同大多数 Java 集合类,
HashSet在多线程环境下如果不做额外同步处理,直接进行读写操作可能会引发数据不一致的问题。如果需要线程安全的 Set,可以使用Collections.synchronizedSet(new HashSet<...>())封装得到。
使用示例:
java
import java.util.HashSet;
public class Main {
public static void main(String[] args) {
HashSet<String> set = new HashSet<>();
set.add("Apple");
set.add("Banana");
set.add("Apple"); // 尝试添加已存在的元素不会成功
System.out.println(set.contains("Cherry")); // 输出 false,集合中不含 "Cherry"
set.add("Cherry");
System.out.println(set); // 输出 [Apple, Banana, Cherry],注意元素的顺序可能变化
}
}Collection - Set - TreeSet
基于红黑树(Red-Black Tree)数据结构的。支持有序性操作。
主要特点:
- 排序性:
TreeSet中的元素是自动排序的,可以按照元素的自然顺序(如Integer、String等),也可以通过提供自定义的Comparator来进行排序。 - 无重复元素:作为集合的一种,
TreeSet不允许存储重复的元素。 - 高效查找:由于内部采用红黑树的数据结构,对于添加、删除和查找操作的时间复杂度都是O(log n)。
- 导航方法:由于实现了NavigableSet接口,所以提供了丰富的导航方法,如 ceiling(), floor(), higher(), lower() 等,可以根据给定元素获取其上界、下界以及相邻的更大或更小的元素。
使用示例:
java
import java.util.TreeSet;
public class Main {
public static void main(String[] args) {
TreeSet<String> treeSet = new TreeSet<>();
treeSet.add("Apple");
treeSet.add("Banana");
treeSet.add("Cherry");
// 自动排序
System.out.println(treeSet); // 输出:[Apple, Banana, Cherry]
// 添加重复元素无效
treeSet.add("Apple");
System.out.println(treeSet); // 输出不变
// 查找元素
if (treeSet.contains("Banana")) {
System.out.println("找到了Banana");
}
}
}如果需要自定义排序,可以在创建TreeSet时传入一个Comparator对象:
java
TreeSet<String> treeSet = new TreeSet<>(new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return s1.length() - s2.length(); // 按字符串长度排序
}
});Collection - Set - LinkedHashSet
同时实现了 Set 和 Map 接口,结合了 HashSet 和 LinkedList 的特性。 LinkedHashSet 存储的元素是无序且唯一的,但它维护了一个元素插入的顺序,即插入的顺序与迭代输出的顺序一致。
主要特点:
- 唯一性:同
HashSet一样,LinkedHashSet也不允许存储重复元素,通过调用元素的hashCode()和equals()方法确保元素的唯一性。 - 有序性:虽然不像 List 那样严格保持插入顺序,但是 LinkedHashSet 通过链接每个元素(使用双向链表)来维护元素的插入顺序。这意味着当你遍历
LinkedHashSet时,元素将会按照它们被添加到集合中的顺序出现。 - 查找速度:由于它底层依赖于哈希表(
HashMap)和双向链表,插入、删除和查找操作的平均时间复杂度接近 O(1)。 - 不是线程安全的:
LinkedHashSet同样不是线程安全的,如果需要在多线程环境下使用,需要自行进行同步控制。
使用示例:
java
import java.util.LinkedHashSet;
public class Main {
public static void main(String[] args) {
LinkedHashSet<String> set = new LinkedHashSet<>();
set.add("Apple");
set.add("Banana");
set.add("Apple"); // 尝试添加已存在的元素不会成功
set.add("Cherry");
System.out.println(set); // 输出 [Apple, Banana, Cherry],按插入顺序排列
}
}Collection - Queue - LinkedList
采用链表(Linked List)的方式来存储元素。 LinkedList 可分为双端链表和单端链表,Java 中的 LinkedList 类实现的是双端链表,每个节点(Node)包含元素和指向前后节点的引用。
主要特点:
- 结构特性:
LinkedList中的元素在内存中并非连续存储,而是分散存储,每个元素(节点)之间通过引用相连。 - 动态增长:
LinkedList的大小是可变的,可以在头部、尾部或任何位置方便地添加或删除元素。 - 访问速度:由于链表的特性,访问 LinkedList 中的元素需要从头节点或尾节点开始沿着引用遍历,因此通过索引访问元素的时间复杂度为 O(n)。但在首尾节点处的添加和删除操作(
addFirst(),addLast(),removeFirst(),removeLast())的时间复杂度为 O(1)。 - 插入和删除操作:在
LinkedList中,无论在哪一个位置插入或删除元素,只需要改变相应节点的前后引用,所以插入和删除操作(非首尾位置)的时间复杂度也为 O(1),相比数组(如ArrayList)在中间位置插入或删除更高效。 - 允许重复元素:
LinkedList同样可以存储重复的元素,并且根据元素加入的顺序形成链式结构。 - 不是线程安全的:
LinkedList对象在多线程环境下直接进行读写操作时同样可能存在线程安全问题,需要用户自行进行同步控制。
使用示例:
java
import java.util.LinkedList;
public class Main {
public static void main(String[] args) {
LinkedList<String> list = new LinkedList<>();
list.addFirst("Apple"); // 在头部添加元素
list.addLast("Banana"); // 在尾部添加元素
list.add(1, "Cherry"); // 在索引为1的位置插入元素
System.out.println(list.pollFirst()); // 输出并移除第一个元素 "Apple"
System.out.println(list.peekLast()); // 输出但不移除最后一个元素 "Banana"
list.removeLastOccurrence("Cherry"); // 移除最后一个出现的"Cherry"
System.out.println(list); // 输出 ["Banana"]
}
}Collection -Queue - PriorityQueue
实现了 Queue 接口,主要用于存储优先级队列中的元素。 在这个队列中,元素被赋予优先级,每次取出的元素都是具有最高优先级(或者说最低优先级,取决于如何定义优先级顺序)的元素。
主要特点:
- 排序原则:
PriorityQueue默认按自然顺序(Comparable自然排序)或提供的Comparator来对元素进行排序。这意味着元素必须实现Comparable接口,或者在创建PriorityQueue时提供一个Comparator。 - 堆结构:
PriorityQueue在内部采用了一种称为“堆”的数据结构来组织元素,堆是一种特殊的完全二叉树,满足父节点的值总是小于(或大于)其子节点的值,这确保了队列顶部始终是优先级最高的元素。 - 插入与删除操作:插入操作(add 或 offer)将元素放在正确的位置以维持堆的性质,删除操作(poll 或 remove)会移除并返回优先级最高的元素。
- 无界与有界:默认情况下,
PriorityQueue是无界的,但如果在构造时提供了初始容量和最大容量参数,那么它可以变为有界的队列。 - 非线程安全:和许多 Java 集合类一样,
PriorityQueue本身不是线程安全的,如果需要在多线程环境中使用,需要外部进行同步控制。
使用示例:
java
import java.util.PriorityQueue;
public class Main {
public static void main(String[] args) {
// 创建一个优先级队列,元素按照升序排序
PriorityQueue<Integer> pq = new PriorityQueue<>();
// 添加元素
pq.offer(3);
pq.offer(1);
pq.offer(5);
// 按优先级(升序)取出元素
while (!pq.isEmpty()) {
System.out.println(pq.poll()); // 输出顺序将是 1, 3, 5
}
}
}如果你想自定义优先级顺序,可以创建一个实现了 Comparator 的类,然后在构造 PriorityQueue 时传入这个 Comparator:
java
import java.util.Comparator;
import java.util.PriorityQueue;
class Person {
String name;
int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" + "name='" + name + '\'' + ", age=" + age + '}';
}
}
Comparator<Person> personComparator = Comparator.comparingInt(Person::getAge);
PriorityQueue<Person> queue = new PriorityQueue<>(personComparator);
queue.offer(new Person("Alice", 25));
queue.offer(new Person("Bob", 30));
queue.offer(new Person("Charlie", 20));
while (!queue.isEmpty()) {
System.out.println(queue.poll()); // 根据年龄从小到大输出
}Java面向对象(高级)- 多线程
Java中线程的状态与转换
新建(New)
当线程对象对创建后,例如Thread t = new Thread(),它并没有开始运行,处于新建状态。
可运行(Runnable)
一个新创建的消除并不自动开始运行,调用
start()方法处于Ready,调用run()方法处于Running,因此包含了操作系统线程状态中的 Ready 和 Running。阻塞(Blocked)
暂时停止执行,获取一个排它锁,将资源交给其他线程使用,待其他线程释放了锁就会结束此状态,属于被动描述状态。
等待(Waiting)
- 无限等待(Waiting):调用
Object.wait()方法,属于主动的挂起线程。 - 有限等待(Time Waiting):调用
Thread.sleep()方法,属于主动的睡眠线程。
- 无限等待(Waiting):调用
死亡(Terminated)
当线程完成执行或被中断,它就进入死亡状态。
Java中实现多线程的两种方式:
继承
Thread类javapublic class MyThread extends Thread { private String name; public MyThread(String name) { this.name = name; } @Override public void run() { System.out.println("线程 " + name + " 正在运行..."); // 这里执行具体的线程任务 } public static void main(String[] args) { MyThread thread1 = new MyThread("线程1"); thread1.start(); MyThread thread2 = new MyThread("线程2"); thread2.start(); } }实现
Runnable接口(无返回值)javapublic class RunnableTask implements Runnable { private String taskName; public RunnableTask(String taskName) { this.taskName = taskName; } @Override public void run() { System.out.println("Runnable任务 " + taskName + " 正在运行..."); // 这里执行具体的线程任务 } public static void main(String[] args) { RunnableTask task1 = new RunnableTask("任务1"); Thread thread1 = new Thread(task1); thread1.start(); RunnableTask task2 = new RunnableTask("任务2"); Thread thread2 = new Thread(task2); thread2.start(); } }实现
Callable接口(可以有返回值通过java.util.concurrent.FutureTask进行封装)javaimport java.util.concurrent.Callable; import java.util.concurrent.FutureTask; public class CallableTask implements Callable<String> { private String taskName; public CallableTask(String taskName) { this.taskName = taskName; } @Override public String call() throws Exception { System.out.println("Callable任务 " + taskName + " 正在运行..."); // 执行计算任务并返回结果 return "任务 " + taskName + " 完成后的结果"; } public static void main(String[] args) { CallableTask task = new CallableTask("可返回结果的任务"); FutureTask<String> futureTask = new FutureTask<>(task); Thread thread = new Thread(futureTask); thread.start(); try { // 获取线程执行结果 String result = futureTask.get(); System.out.println("Callable任务的结果:" + result); } catch (Exception e) { e.printStackTrace(); } } }
Java 中的锁
//todo 待补充