Java笔记
注:DeepSeek老师参与部分笔记的整理
QQ:2774118934
1.在java中,使用fanal来声明一个常量.习惯上,常量名使用全大写
final int ABC=10;
2.设置类常量在一个类的多个方法中使用(类变量同理)
public class FirstSamp{
public static final int a = 1000;
public static void main(String[] args){
System.out.println("Hello World!");
System.out.println(a);
display();
}
public static void display(){
System.out.println(a);
}
}
3.在函数内部对类变量做出的修改,在函数外也生效
public class FirstSamp{
public static final int a = 1000;
public static void main(String[] args){
System.out.println("Hello World!");
System.out.println(a);
display();
}
public static void display(){
System.out.println(a);
}
}
4.如果一个常量被声明为public,那么其他类的方法也可以使用这个常量
5.Hello World!
public class class2{
public static void main(String[] args){
System.out.println("Hello World!");
}
}
6.关于main(String[] args)中String[] arg的作用:
String[]: 表示这是一个字符串数组args: 是参数的名称(可以自定义)
作用:当你通过命令行运行 Java 程序时,可以在程序名后面添加参数,这些参数会被存储在args数组中。
总结:String[] args的主要作用是让 Java 程序能够接收并处理命令行参数。它是 Java 规范中 main 方法的固定写法,即使不传参也必须保留。
7.整数被0除将会产生一个异常,而浮点数被0除将会得到无穷大或NaN结果
8.防止在不同环境中出现浮点溢出(一般用不到)
可以把main方法标记为public static strictfp void main(String[] args)
那么,main方法中所有的指令都讲使用严格的浮点计算。(不仅适用于方法,对于类也同样适用)
9.static关键字
含有static关键字的成员(变量或方法)属于类本身,在类加载时分配(全局唯一)
public class test1{
public static void main(String[] args) {
System.out.println("hello world");
}
}
非static的方法属于对象,是类的实例,每次new时分配,每个实例独立
Car myCar = new Car("Tesla");
myCar.drive(); // 必须通过实例调用
10.数据存储类型
boolean:布尔值
String:字符串类型
null:null 类型可以直接赋值给所有引用类型的变量,表示不会指向任何有效对象
11.Java允许同时给两个变量赋值
public class P1 {
public static void main(){
int a,b;
a=b=20;
System.out.println(a);
System.out.println(b);
}
}
12.与C语言相同的部分
++x与x++- 逻辑运算符
&&,||,! - 条件表达式
a = 1>0 ? true : false - 选择结构语法
- switch语句(不能是长整型)
- while和for循环结构
13.Java 位运算符与位表达式(实际开发用的少)
位运算符
| 运算符 | 名称 | 描述 | 示例 |
|---|---|---|---|
& |
按位与(AND) | 两个操作数的对应位都为1时结果位才是1 | a & b |
| |
按位或(OR) | 两个操作数的对应位有一个是1时结果位就是1 | a | b |
^ |
按位异或(XOR) | 两个操作数的对应位不同时结果位是1 | a ^ b |
| `` | 按位取反(NOT) | 对操作数的每一位取反 | a |
<< |
左移 | 将操作数的所有位向左移动指定位数,低位补0 | a << n |
>> |
带符号右移 | 将操作数的所有位向右移动指定位数,高位补符号位 | a >> n |
>>> |
无符号右移 | 将操作数的所有位向右移动指定位数,高位补0 | a >>> n |
位表达式示例
// 按位与(&)
int a = 0b1010; // 10
int b = 0b1100; // 12
int result = a & b; // 0b1000 (8)
// 按位或(|)
int a = 0b1010;
int b = 0b1100;
int result = a | b; // 0b1110 (14)
// 按位异或(^)
int a = 0b1010;
int b = 0b1100;
int result = a ^ b; // 0b0110 (6)
// 按位取反()
int a = 0b00001010; // 10
int result = a; // 0b11110101 (-11)
// 左移(<<)
int a = 0b00001010; // 10
int result = a << 2; // 0b00101000 (40)
// 带符号右移(>>)
int a = 0b10001000; // -120
int result = a >> 2; // 0b11100010 (-30)
// 无符号右移(>>>)
int a = 0b10001000; // -120
int result = a >>> 2; // 0b00100010 (34)
应用场景
- 位掩码操作
final int FLAG_A = 0b0001;
final int FLAG_B = 0b0010;
int flags = 0;
// 设置标志位
flags |= FLAG_A;
// 检查标志位
boolean hasFlagA = (flags & FLAG_A) != 0;
// 清除标志位
flags &= FLAG_A;
- 快速计算
// 乘以2^n
int a = 5;
int doubled = a << 1; // 10
// 除以2^n
int b = 20;
int half = b >> 1; // 10
- 变量交换
int x = 5, y = 3;
x = x ^ y;
y = x ^ y;
x = x ^ y;
// 现在x=3, y=5
注意事项
- 只适用于整数类型(int, long, short, byte, char)
- 运算符优先级较低,建议使用括号明确运算顺序
- 无符号右移(>>>)仅适用于int和long类型
14. 字符串操作
1 字符串方法
1.1 length()获取字符串长度
public class P2 {
public static void main(String[] args) {
String str = new String("计算坤科学");
System.out.println(str.length());
}
}
1.2 str.charAt(index)返回索引位置对应的字符
public class P2 {
public static void main(String[] args) {
String str = new String("计算坤科学");
System.out.println(str.charAt(4));
}
}
1.3 子串操作
- int indexOf(String str,int from):查找子串str在当前字符串中从from索引开始首次出现的位置。不存在则返回-1
- int lastIndexOf(String str,int from):查找子串str在当前字符串内从from索引开始(从索引往前找)最后一次出现的位置。不存在则返回-1
- String substring(int begin,int end):字符串切片,截取从begin(包含)到end(不包含)之间的字符
public class P2 {
public static void main(String[] args) {
String str = new String("计算坤科学");
System.out.println(str.charAt(4));
System.out.println(str.indexOf("坤",0));
System.out.println(str.lastIndexOf("计",4));
System.out.println(str.substring(0,3));
}
}
1.4 字符串的比较
- boolean equals(Object obj):如果obj为String类型则比较内容是否相同,相同则返回true,否则返回flase。(区分大小写)
- boolean equalslgnoreCase(String str):比较内容是否相同(不区分大小写)
- int compareTo(String str):依据对应字符的Unicode编码比较str和当前字符串的大小。(区分大小写)
当前字符串 > str:返回正整数
当前字符串 < str:返回负整数
当前字符串 == str:返回0 - int compareTolgnoreCase(String str):与上一方法类似(不区分大小写)
- boolean startsWith(String begin):判断当前字符串的开头是否为begin
- boolean endsWith(String end):判断当前字符串的结尾是否为end
public class P2 {
public static void main(String[] args) {
String str = new String("<title> 这是一个html标题 </title>");
if(str.startsWith("<title>") && str.endsWith("</title>")){
System.out.println("条件成立");
}
}
}
1.5 修改字符串
- String toLowerCase():把当前字符串的所有字母转为小写
- String toUpperCase():把当前字符串的所有字母转为大写
- String replace(String oldChar,String NewChar):把所有的oldChar字符串替换为newChar
- String trim():去掉当前字符串对象的首位空白字符(一般为空格)
1.6 修改字符串类型
1.6.1 字符数组转字符串
形式1:直接转换
- String s = new String(array);
public class P2 {
public static void main(String[] args) {
char[] array = {'H','E','L','L','O'};
// 字符数组直接打印也是HELLO
System.out.println(array);
String s = new String(array);
System.out.println(s);
// 输出HELLO
}
}
形式2:保留原有字符数组的格式
- Arrays.toString(array)
import java.util.Arrays;
public class P2 {
public static void main(String[] args) {
char[] array = {'H','E','L','L','O'};
// 打印结果为[H, E, L, L, O]
System.out.println(Arrays.toString(array));
String s = new String(array);
System.out.println(s);
}
}
1.6.2 字符串转字符数组
- str.toCharArray()
import java.util.Arrays;
public class P2 {
public static void main(String[] args) {
String str = "hello";
// 字符串转字符数组
char[] array = str.toCharArray();
System.out.println(array);
System.out.println(Arrays.toString(array));
// 输出
// hello
// [h, e, l, l, o]
}
}
1.7 str.split(分割符):分割字符串
1.8 StringBuffer类
StringBuffer类的一些方法和String类相似,在功能上与String类一样,区别在于String对象的修改其实是在内存新开一块区域,而StringBuffer对象的每次修改只是修改变量自己
1.8.1 StringBuffer类的构造方法
- StringBuffer():创建一个不带字符的空的StringBuffer对象,其初始容量是16个字符
- StringBuffer(int length):构造一个不带字符的字符串缓冲区,但指定其初始容量大小为length。
- StringBuffer(String str):用于构造一个字符串缓冲区
StringBuffer str1 = new StringBuffer();
StringBuffer str2 = new StringBuffer("word");
与String不同,创建StringBuffer对象必须使用它的构建方法。例如,StringBuffer="abc"是不允许的。创建的StringBuffer对象,除了分配实际串的长度,还另外分配了16字节的缓冲区
1.8.2 StringBuffer类的常用方法
| 功能分类 | 方法 | 说明 | 示例 |
|---|---|---|---|
| 长度与容量 | length() |
返回当前字符数 | sb.length() |
capacity() |
返回当前容量(内部 char 数组大小) | sb.capacity() |
|
ensureCapacity(int minimumCapacity) |
确保容量至少为指定值 | sb.ensureCapacity(50) |
|
setLength(int newLength) |
设置字符长度,超长截断,超短补 \0 |
sb.setLength(5) |
|
| 字符操作 | charAt(int index) |
返回指定索引的字符 | sb.charAt(0) |
setCharAt(int index, char ch) |
修改指定索引的字符 | sb.setCharAt(0,'A') |
|
| 添加/插入 | append(...) |
在末尾追加字符串/字符/数值 | sb.append("Hello") |
insert(int offset, ...) |
在指定位置插入字符串/字符/数值 | sb.insert(2,"ABC") |
|
| 删除 | delete(int start, int end) |
删除指定区间字符 | sb.delete(2,5) |
deleteCharAt(int index) |
删除指定索引字符 | sb.deleteCharAt(0) |
|
| 替换 | replace(int start, int end, String str) |
替换指定区间字符 | sb.replace(0,2,"Hi") |
| 反转 | reverse() |
反转当前字符串 | sb.reverse() |
| 转换 | toString() |
转成 String 对象 |
String s = sb.toString() |
| 搜索 | indexOf(String str) |
返回第一次出现索引 | sb.indexOf("abc") |
indexOf(String str, int fromIndex) |
从指定位置开始搜索 | sb.indexOf("abc",5) |
|
lastIndexOf(String str) |
返回最后一次出现索引 | sb.lastIndexOf("abc") |
|
lastIndexOf(String str, int fromIndex) |
从指定索引向左搜索 | sb.lastIndexOf("abc",10) |
|
| 其他 | trimToSize() |
将容量缩小到字符长度 | sb.trimToSize() |
1.9 isEmpty()判断字符字符串是否为空(长度为0)
- 若字符串为空串则返回
false - 不为空串(length>0)则返回
true
15. for循环的另一种形式,foreach循环
for!
循环变量必须与数组的数据类型相同
- for(类型 循环变量 : 可迭代对象)
public class P2 {
public static void main(String[] args) {
String str = "hello";
char[] array = str.toCharArray();
for(char i : array){
System.out.println(i);
}
}
}
- data.forEach
@PostMapping("/basicdata")
public UniversalResponse<Map<String, Object>> receiveData(
@RequestBody Map<String, Object> data
) {
System.out.println("请求成功(后端输出)");
data.forEach((k, v) -> System.out.println(k + ":" + v));
return UniversalResponse.success("请求成功");
}
注:还有升级plus版,去看下面的语法糖
16 数组
1 一维数组
1.1 一维数组的声明
第一种
int[] array;
第二种
int array[];
1.2 一维数组的创建
完成一维数组的声明后,便可进行数组的创建。创建数组即指定这个数组可以存放多少个元素,并分配给对应大小的内存空间。
Java中可以使用new关键字来对数组进行空间的分配,语法格式如下
数组名称 = new 数据类型[数组长度]
声明时也可以进行空间分配的工作,语法格式如下
数据类型[] 数组名 = new 数据类型[ 数组长度 ]
int[] scores = new int[10];
完成数组大小的声明后,便无法再修改
1.3 一维数组的初始化
- 静态初始化
int[] array = {1,2,3,4,5}; // 简洁形式
int[] array = new int[]{1,2,3,4,5}; // 完整形式
- 动态初始化(先指定长度,后赋值)
int[] arr = new int[5]; // 先创建长度为5的数组
arr[0] = 1; // 后赋值
arr[1] = 2;
// ...
注:静态初始化不能先声明再赋值
int[] arr;
arr = {1, 2, 3}; // 编译错误
2 数组的操作
2.1 Arrays.equalse(arrayA,arrayB):比较数组
相同则返回true,不同则返回false
2.2 System.arraycopy(arrayA,0,arrayB,0,a.length):复制数组
- arrayA:来源数组的名称
- 0:来源数组起始位置
- arrayB:目的数组名称
- 0:目的数组起始位置
- a.length:来源数组被复制的元素数量
2.3 int array.length: 返回数组的长度
2.4 Arrays.sort(array):数组排序
17 集合框架(各个接口)
集合与数组的区别:
- 1.数组必须指定长度,集合是动态长度
- 2.数组两种类型都可以(基本类型与类),集合只能存储类。不过基本类型都有对应的类类型
- 3.自动装箱、拆箱原理:基本类型与类类型之间的自动转换
int a = 2;
Integer b = a;//基本类型->类类型:装箱
int c = b;//类类型->基本类型:拆箱
1 Collection接口(单个对象的存储结构)
有List、Set两个子接口
List(有序)
ArrayList
Set(不重复)
HashSet
1.1 Collection接口中定义的方法
| 方法声明 | 功能描述 |
|---|---|
boolean add(E e) |
将对象添加入集合 |
boolean remove(Object o) |
从这个集合中移除指定元素的一个实例 |
void clear() |
从这个集合中移除所有的元素 |
Iterator<E> iterator() |
返回此集合中的元素的迭代器 |
int size() |
返回此集合中的元素的数目 |
Object[] toArray() |
返回包含此集合中所有元素的数组 |
boolean contains(Object o) |
返回 true,当且仅当这个集合包含至少一个元素 |
1.1.1 iterator迭代器的方法
- iterator.hasNext() :检查集合中是否还有下一个元素可供遍历。返回true/false,常用于while循环条件
- iterator.next():返回当前迭代器指向的元素,并将迭代器的指针移动到下一个元素。
1.2 接口实例的创建
import java.util.ArrayList;
import java.util.Collection;
public class CollectionExample {
public static void main(String[] args) {
// 创建Collection实例(使用ArrayList实现)
Collection<String> collection = new ArrayList<>();
}
}
1.3 Collection接口的主要功能方法
- 单元素的添加、删除功能
- boolean add(Object o):添加一个元素到尾部
- boolean remove(Object o):删除指定元素o
- 查询功能
- int size():返回元素的个数
- boolean isEmpty():判断集合是否为空
- boolean contains(Object o):查找集合中是否包含指定元素
- Iterator iterator():迭代器,集合的专用遍历方式
- 组功能(作用于元素组或整个集合)
- boolean containsAll (Collection c):判断当前集合中是否包含c中所有元素
- boolean addAll(Collection c):将c中包含的所有元素添加到当前集合中
- void removeAll(Collection c):从当前集合中删除包含在c中的元素
- void retainAll(Collection c):从当前集合中删除不包含在c中的元素
- void clear():清空集合元素
- 把集合转换为数组
- Object[] toArray():返回到一个内含集合所有元素的array。
- Object[] toArray(Object[] a):返回到一个内含集合所有元素的array。运行期间返回的array与参数a的类型相同
简单遍历
for(String i : collection){
System.out.println(i);
}
2 List接口(顺序)
2.1 常用方法
泛型<>:规定集合中对象的类型
import java.util.ArrayList;
import java.util.List;
public class P3 {
public static void main(String[] args){
// 泛型<>:规定集合中对象的类型
// 创建集合
List<String> list = new ArrayList<String>();
// 添加对象
list.add("张三");
list.add("李四");
list.add("王五");
// 遍历集合
for (String s : list) {
System.out.println(s);
}
}
}
初始化时就添加元素
- 构造方法+Arrays.asList
import java.util.ArrayList;
import java.util.Arrays;
public class Demo {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
System.out.println(list); // 输出: [A, B, C]
}
}
- 匿名内部类(双括号初始化)
注:这种方式称为 双括号初始化,语法简洁,但内部会生成一个匿名类,可能稍微影响性能和序列化。
ArrayList<String> list = new ArrayList<>() {{
add("A");
add("B");
add("C");
}};
System.out.println(list); // 输出: [A, B, C]
2.2 List接口的主要方法
- void add(int index,E element):在列表中指定的位置上插入指定的元素
- E remove(int index):移除此列表中指定位置的元素
- E get(int index):返回此列表中指定位置的元素
- E set(int index,E element):用指定元素替换该列表中指定位置的元素
- List subList(int fromindex,int toindex):返回从索引fromindex到toindex(不包含)处所有元素集合组成的子集合
- int indexOf(Object o):返回此列表中该元素第一次出现的位置
注:E 是泛型参数,表示该方法可以处理任何类型的 element**
3 Set接口(不重复,但不保证有序)
Set接口有两个子接口
HashSet类:快速存取结合中的元素(无序)
TreeSet类:(有序)
3.1 常用方法
创建、添加、遍历
import java.util.HashSet;
import java.util.Set;
public class P4 {
public static void main(String[] args){
// 创建集合
Set<String> set = new HashSet<String>();
// 添加对象
set.add("张三");
set.add("张三");
set.add("李四");
set.add("张三");
set.add("李四");
set.add("王五");
// 遍历集合
for (String string : set) {
System.out.println(string);
}
}
}
李四
张三
王五
注:add方法有返回值(true/false),Set集合会添加第一次出现的值,返回true,不添加再次出现的值,返回false。详细查阅API文档
3.2其他方法
| 方法 | 介绍 |
|---|---|
| contains(Object o) | 判断HashSet中是否存在指定值(时间复杂度O(1)) |
| Collections.min(hashSet) | 返回Hashset中的最小值。(HashSet |
| Collections.max(hashSet) | 返回Hashset中的最大值。(HashSet |
4 Map接口(键值对的存储结构)
- HashMap:高效存取键值对
- TreeMap:存储排序后的键值对
- LinkedHashMap:会保持插入顺序(或访问顺序,如果构造时指定)。LinkedHashMap 是 HashMap 的一个子类,它通过维护一个双向链表来记录插入顺序或访问顺序。
4.1 常用方法
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class P4 {
public static void main(String[] args){
// 创建集合
Map<String,String> map = new HashMap<String,String>();
// 添加键值对
map.put("1", "张三");
map.put("2", "李四");
map.put("3", "王五");
// 根据key获取value
String name = map.get("1");
System.out.println(name);//打印张三
// 遍历
Set<String> set = map.keySet();//获取key的集合
// 然后通过遍历key获取对应的value
for(String key:set){
System.out.println(key+" "+map.get(key));
}
}
}
4.2 常用函数
V的意思是value
| 方法 | 介绍 |
|---|---|
| void clear() | 从集合中移除所有映射 |
| V put(K key,V value) | 将键值对放入字典中 |
| V get(Object key) | 返回指定键对应的值 |
| boolean containsKey(Obejct key) | 如果字典中包含指定键的映射 ,则返回true |
| boolean containsValue(Object value) | 如果字典中包含一个或多个该值,则返回true |
| Collection |
返回该字典中包含的值的Collection集合 |
| Set |
返回此字典中包含的键的集合 |
| V getOrDefault(Object key, V defaultValue) | 获取键对应的值,如果不存在则返回defaultValue |
V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) |
根据现有键值对计算新值。 |
compute函数示例
Map<String, Integer> map = new HashMap<>();
map.put("apple", 5);
// 示例1:键存在时修改
map.compute("apple", (k, v) -> v + 10);
System.out.println(map); // {apple=15}
// 示例2:键不存在时插入
map.compute("banana", (k, v) -> 3);
System.out.println(map); // {apple=15, banana=3}
// 示例3:返回null删除键
map.compute("apple", (k, v) -> null);
System.out.println(map); // {banana=3}
4.3 给Map设置初始值
使用双括号初始化(匿名内部类)
import java.util.Map;
import java.util.TreeMap;
public class P7 {
public static void main(String[] args){
Map<String,String> map = new TreeMap<String,String>(){{
put("1", "abc");
put("2", "aaa");
put("0", "bbb");
}};
for (String string : map.keySet()) {
System.out.println(string + "\t" + map.get(string));
}
}
}
匿名内部类初始化结构:
new 父类() {
// 这是匿名子类
{
// 这是实例初始化代码块
}
}
4.4性能最好的遍历方法
用 entrySet()
Map<String, Integer> map = new HashMap<>();
map.put("a", 1);
map.put("b", 2);
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + " : " + entry.getValue());
}
Map.Entry 就是 Map 里的一对 key-value 键值对对象。
map.entrySet() 的作用是:把 Map 里的所有键值对(key-value)打包成一个 Set 返回。
Java 8 Lambda 写法
map.forEach((key, value) -> {
System.out.println(key + " : " + value);
});
✔ 简洁
✔ 可读性好
✔ 本质还是 entrySet
4.5ArrayList初始化列表
方法1:使用 Arrays.asList()
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
// 最简洁的方式
ArrayList<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
// 或者分步
List<Integer> temp = Arrays.asList(1, 2, 3, 4, 5);
ArrayList<Integer> list2 = new ArrayList<>(temp);
方法2:使用 List.of()(Java 9+)
// Java 9 及以上版本
ArrayList<Integer> list = new ArrayList<>(List.of(1, 2, 3, 4, 5));
5 Properties类
Properties类是以键值对的方式存储字符串类型的,通常存储应用程序的配置信息,如用户的设置信息。
Properties类继承于HashTable,它可以保存在输入流中,也可以在输入流中加载
5.1 常用方法
| 方法 | 说明 |
|---|---|
| getProperty(String key) | 用指定的键在此属性列表中搜索属性。通过参数key,得到对应的value |
| load(InputStream inStream) | 从输入流中读取属性列表(键和元素对)。通过对指定的文件进行装载来获取该文件中的所有键值对,以供getProperty(Stringkey)来搜索 |
| setProperty(String key,String value) | 通过调用 HashTable的put方法 的方式来设置键值对 |
| store(OutputStream out,String comments) | 以适合使用load方法的特殊格式(Property格式),将字典(键和值)写入输出流。与load方法相反,该方法在基类中提供。(保存) |
| clear() | 清除所有装载的键值对,该方法在基类中提供 |
Enumeration<?>propertyNames() |
返回Properties对象中所有key值的枚举集合。类似于 Map 的 keySet() 方法,但返回的是 Enumeration 而不是 Set。注:在较新的Java版本中,也可以使用stringPropertyNames()方法,它返回一个Set |
18 标准输入输出
- 关于print与println的区别: print()默认不换行,println默认自动换行
- 也可以像这样输出(与C语言的printf类似)
System.out.printf("Name: %s, Price: %.2f%n", name, price);
19 Java中类的修饰符
在Java中,类的修饰符主要分为两大类:访问控制修饰符和非访问控制修饰符。它们用于定义类在程序中的可见性和行为特性。
1 访问控制修饰符
访问控制修饰符决定了其他类或包对当前类的访问权限。
1.1 public-访问权限最宽的修饰符
- 被
public修饰的类可以在任何地方被访问。(不仅可以跨类访问,还可以跨包访问) - 一个Java源文件中只能有一个公共类,并且该公共类的名称必须与源文件名相同。
- 即使内部类可以声明为 public,但外部类仍然只能有一个 public 类。
1.2 protected
- 这个修饰符不能用于修饰类,只能用于修饰类的成员(变量、方法、内部类)。
- 当前类、同包类、子类都可以访问(即使子类在不同包)
- 子类可以继承被protected修饰的成员
- 常用于继承
1.3 private-访问权限最窄的修饰符
- 这个修饰符也不能用于修饰外部类,只能用于修饰类的成员或内部类。
- 被
private修饰的内部类只能在其外部类中被访问。 - 被
private修饰的成员,仅限本类访问,外部类(包括子类)无法直接访问。 - 子类不能继承被
private修饰的成员
1.4 默认 (无修饰符)
- 如果一个类没有使用任何访问控制修饰符,它就具有包级私有 (package-private) 访问权限。
- 这意味着该类只能在同一个包内被访问。
1.5 访问权限对比图
| 修饰符 | 当前类 | 同包类 | 子类(不同包) | 其他包的非子类 |
|---|---|---|---|---|
public |
✅ | ✅ | ✅ | ✅ |
protected |
✅ | ✅ | ✅ | ❌ |
默认(无修饰符) |
✅ | ✅ | ❌ | ❌ |
private |
✅ | ❌ | ❌ | ❌ |
2 非访问控制修饰符
非访问控制修饰符用于定义类的特殊行为或特性。
2.1 final
- 当一个类被
final修饰时,它被称为最终类。 - 最终类不能被继承,这意味着你不能创建它的子类。
- 这通常用于防止类的行为被修改或扩展,例如
String类就是final的。
2.2 abstract
- 当一个类被
abstract修饰时,它被称为抽象类。 - 抽象类不能直接被实例化(即不能使用
new关键字创建对象)。 - 抽象类可以包含抽象方法(没有具体实现的方法)和非抽象方法。
- 如果一个类包含抽象方法,那么该类必须声明为抽象类。
- 抽象类的主要目的是作为其他类的父类,为子类提供一个通用的模板,并强制子类去实现其抽象方法。
注: 抽象方法不需要具体实现!
2.3 strictfp (自 Java 1.2 引入)
- 当一个类被
strictfp修饰时,它强制所有浮点运算都遵循 IEEE 754 浮点标准。 - 这意味着在不同硬件平台上,浮点运算的结果将保持一致。
- 这个修饰符通常用于对浮点精度要求非常高的科学计算或金融应用中。
2.4 static
static修饰符不能直接用于修饰外部类。- 它主要用于修饰内部类(称为静态嵌套类或静态内部类)。
- 静态内部类不依赖于其外部类的实例。 它可以直接通过外部类名访问,类似于静态成员。
20 同一文件夹下的其他Java文件可能会对该文件的运行造成干扰。
即Java会自动读取在同一文件夹下的Java文件,若该文件夹下存在与该文件的一个类名相同的文件,则可能会出现编译错误
21 Java中的类
1 语法格式
[ 修饰符 ] class 类名{
零个到多个构造器的定义...
零个到多个属性...
零个到多个方法...
}
注: 被static修饰的成员(静态成员)不能直接访问非静态成员(实例成员),但可以通过对象实例间接访问非静态成员。
例:
public class Example {
int nonStaticField = 10; // 非静态成员
static int staticField = 20; // 静态成员
void nonStaticMethod() {
System.out.println(staticField); // 非静态方法可以访问静态成员
}
static void staticMethod() {
// System.out.println(nonStaticField); // 错误!静态方法不能直接访问非静态成员
Example obj = new Example();
System.out.println(obj.nonStaticField); // 正确!通过对象实例间接访问
}
public void main(String[] args){
nonStaticMethod();//非静态方法
staticMethod();//静态方法
}
}
2 构建方法(类的初始化)
在使用某个类创建对象时,Java会自动调用这个类的构建方法。如果用户定义了构建方法,则会调用用户定义的新构建方法。否则会自动生成一个构建方法
2.1 声明构建方法的语法格式
[public] 类名 ([形参列表]){
// 方法体
}
2.2 示例
public class Example2 {
public static void main(String[] args){
System.out.println("文件主函数被触发");
Dog ex = new Dog();
}
}
class Dog {
public Dog(){
System.out.println("构造方法被触发");
}
}
3 方法重载(多态性的体现)
方法重载需要满足以下两个特点
- 方法名相同
- 方法的参数列表不同(包括参数类型不同、参数个数不同、参数的传入顺序不同)
示例:
public class Calculator {
// 整数相加
public int add(int a, int b) {
return a + b;
}
// 浮点数相加(参数类型不同)
public double add(double a, double b) {
return a + b;
}
// 三个整数相加(参数个数不同)
public int add(int a, int b, int c) {
return a + b + c;
}
public static void main(String[] args) {
Calculator calc = new Calculator();
System.out.println(calc.add(5, 3)); // 调用第一个add方法
System.out.println(calc.add(2.5, 3.7)); // 调用第二个add方法
System.out.println(calc.add(1, 2, 3)); // 调用第三个add方法
}
}
4 this关键字
可以用此方法达到设置默认参数的目的
4.1 代表当前对象,即正在执行方法时所属的对象。
public class Example2 {
public static void main(String[] args){
Point point = new Point(10, 13);
}
}
class Point{
// 声明变量只能从外部类访问
private int x = 0;
private int y = 0;
// 构造方法
public Point(int x,int y){
// 初始化成员变量x与y的值。this代表当前的对象实例
this.x = x;
this.y = y;
System.out.println("x=" + this.x);
System.out.println("y=" + this.y);
}
}
4.2 构建方法的方法重载(多态性)
public class Example2 {
public static void main(String[] args){
// 通过控制创建时传入的变量的个数来选择使用哪个构建方法
Point point = new Point(5,6,4,5);
point.display();
}
}
class Point{
// 声明private类型的成员变量,代表矩阵左上角的点
private int x,y;
// 声明private类型的成员变量,代表矩阵的宽和长
private int width,length;
// 默认构造方法
public Point(){
// 当创建实例时没有初始值时
// 调用当前实例的构造方法,该方法有四个参数
this(1,2,3,4);
// 调用最后一个构建方法
}
public Point(int width,int length){
// 当创建实例时输入了两个初始值时
// 调用当前实例的构造方法,该方法有四个参数
this(1,2,width,length);
}
public Point(int x,int y,int width,int length){
// 当创建实例时输入了四个初始值时
this.x = x;
this.y = y;
this.width = width;
this.length = length;
}
public void display(){
System.out.println("x="+x);
System.out.println("y="+y);
System.out.println("width="+width);
System.out.println("length="+length);
}
}
注:
第一个构造方法 public Point() 中的 this(1,2,3,4) 调用的是最后一个构造方法 public Point(int x, int y, int width, int length),而不是第二个构造方法 public Point(int width, int length)。
这是因为 this(1,2,3,4) 传递了 4 个参数,而最后一个构造方法正好接受 4 个参数,所以会直接匹配调用它。第二个构造方法只接受 2 个参数,所以不会被调用。
5 类中修改静态变量
推荐采用"类修改静态变量"的方法
public class P9 {
// 定义静态变量
private static int x=0;
private static int y=0;
// 定义函数,具有打印功能
void print(){
System.out.println("x="+ x +",y="+y);
}
public static void main(String[] args){
P9 classA = new P9();
classA.print();
// 对象修改静态变量x,不合法(但有效,原因见"注解")
classA.x = 10;
classA.y = 20;
classA.print();
// 类修改静态变量x
P9.x = 15;
P9.y = 25;
classA.print();
}
}
注: 虽然通过实例classA访问静态变量在语法上是允许的,但编译器实际上会将其转换为通过类名访问(即P9.x = 10;)
6 属性封装
推荐把各个属性设置为private修饰符,然后通过内部方法返回值的方式来获取属性值。封装性。
public class Student {
// 定义id等属性,major(专业)
private String id,major;
private int score;
// 用于为学号字段赋值
public void setld(String id){
this.id = id;
// 取id第3~4位,决定专业名称
switch (this.id.substring(2,4)) {
case "01":
major = "物流工程";
break;
case "02":
major = "计算机工程与技术";
break;
}
}
// 用于读取id字段
public String getId(){
return this.id;
}
// 用于读取major字段
public String getMajor(){
// 判断major是否已赋值
if(this.major == null || this.major.isEmpty()){
// 若字段未赋值则返回true
return null;
}else{
return this.major;
}
}
// 设置成绩
public void setScore(int score){
// 大于60,小于100为合格
if(score>=60 && score<=100){
this.score = score;
}else{
System.out.println("无效的成绩");
}
}
// 读取成绩
public int getScore(){
return this.score;
}
}
7 类的继承
7.1 创建子类
package Animal;
public class Dog extends Animal{
public static void main(String[] args){
Dog dog = new Dog();
dog.name = "贵宾犬";//源自继承
dog.shout();
dog.run();//Dog类本身的方法
}
void run(){
System.out.println("the dog is running");
}
}
7.2 继承的原则
- 一个类只能有一个直接父类
- 多个类可继承同一个父类
- 类可以多层继承,例如:
class A{} class B extends A{} class C extends B{} - 子类不能继承被声明为private的成员方法和变量。但能继承父类中被声明为public和protected的成员方法和变量。
- 子类可以继承同包中被默认修饰符修饰的成员变量和方法
8 super关键字
super的主要作用
- 调用父类的构造方法: 在子类构造器中初始化父类部分
- 访问父类的成员: 当子类重写了父类方法或隐藏了父类属性时,访问父类版本
- 解决命名冲突: 当子类和父类有同名成员时明确指定父类成员
8.1 调用父类的构造方法
基本语法格式:
// 调用的构造方法有参数
super(参数列表);
// 调用的构造方法无参数
super();
示例
class Animal {
String name;
public Animal(String name) {
this.name = name;
}
}
class Dog extends Animal {
String breed;
public Dog(String name, String breed) {
super(name); // 调用父类构造器
this.breed = breed;
}
}
重要规则
super()必须是子类构造器中的第一条语句- 如果子类构造器没有显式调用
super(),编译器会自动插入无参的super() - 如果父类没有无参构造器,子类必须显式调用其他构造器
8.2 访问父类成员
访问被覆盖的方法
class Animal {
public void makeSound() {
System.out.println("Animal sound");
}
}
class Dog extends Animal {
@Override
public void makeSound() {
super.makeSound(); // 调用父类方法
System.out.println("Bark bark");
}
}
访问被隐藏的属性
class Parent {
int x = 10;
}
class Child extends Parent {
int x = 20;
void print() {
System.out.println(super.x); // 输出10
System.out.println(this.x); // 输出20
}
}
8.3 注意事项
super只能在非静态方法中使用super不能用在静态上下文中(静态方法、静态代码块)- 在多层继承中,
super只能直接指向其父类,不能"跨代"访问 super不是引用变量,不能像this一样赋值或单独使用
9 成员变量的隐藏
- 在编写子类时,如果声明的成员变量的名字与继承的成员变量的名称相同(类型可以不同),子类就会隐藏所继承的成员变量
10 @Override方法重写
在Java中,有时子类不想原封不动地继承父类的方法,而是想做一定的修改,这就需要采用方法的重写。方法重写也称方法覆盖。其规则有以下几点
- 可被继承的方法能够被重写
- 重写的方法类型要与父类方法类型相一致或是父类方法类型的子类型
- 重写方法名称、参数类型和个数必须与父类方法完全一致
- 在Java中,方法重写(Override)时访问权限修饰符可以不同,但必须遵循 "子类方法的访问权限不能比父类更严格" 的原则。
@Override是Java中的一个注解(Annotation),用于明确表示一个方法是重写(Override)了父类的方法。
class Animal {
public void makeSound() {
System.out.println("Animal sound");
}
}
class Dog extends Animal {
@Override // 明确表示要重写父类方法
public void makeSound() {
System.out.println("Bark bark");
}
}
方法重写可以隐藏继承的方法。通过方法重写,子类可以直接使用父类的属性与方法
public void shout(){
System.out.println(this.name + "is shouting" );//name 是继承的Animal类的属性
}
11 根类Object
在Java中,Object是根类,即所有类的父类,所有对象(包括数组)都实现了这个类的方法和属性。但在应用中一般会将其进行重写再使用
以下是Object类中常见的6中方法
| 方法 | 说明 |
|---|---|
| public final Class getClass() | 返回一个对象的运行时的类 |
| public boolean equals(Object obj) | 判断一个对象是否与参数对象相同 |
| public int hashCode() | 返回对象的哈希值 |
| public String toString() | 返回对象的字符串表示 |
| protected void finalize() throws Throwable | 当垃圾收集器决定在内存中不再对一个对象引用时,会使用这个对象调用该方法 |
| protected Object clone() throws CloneNotSupportedException | 该方法默认实现的是浅拷贝,也叫浅复制。一般子类中会重写这个方法,变为深拷贝 |
除此之外,Object类还有5种方法主要用来同步程序的线程活动,如下:
| 方法 |
|---|
| public final void notify() |
| public final void notifyAll() |
| public final void waie() |
| public final void wait(long timeout) |
| public final void wait(long timeout,int nanos) |
12 深拷贝与浅拷贝
对象的拷贝有浅拷贝和深拷贝。浅拷贝只是拷贝了源对象的地址,所以源对象的值发生变化时,拷贝对象的值也会发生变化。
浅拷贝相当于两个对象共用一个实例。
深拷贝则是拷贝了源对象的所有值,即使源对象的值发生变化时,拷贝对象的值也不会变。
13 抽象类与抽象方法 点击跳转
抽象类的特点如下:
- (1)抽象方法一定在抽象类中,但抽象类中不一定有抽象方法
- (2)抽象类可以省略修饰符abstract,但如果抽象类中包含一个抽象方法,就不能省略该修饰符
- (3)抽象类是对一些类共性的抽取,不能被实例化,但是可以使用多态实现。
- (4)子类继承抽象类后,就必须实现抽象类中所有的抽象方法;如果子类不想实现父类的抽象方法,那么子类必须声明为抽象类。
- (5)抽象类可以直接使用类名访问静态成员。
- (6)在实现接口时,抽象类不需要实现接口的全部方法。
14 通过重载和重写实现多态(静态)
- 方法重载:是指在同一个类中,不同的同名方法具有不同类型的参数、返回值和数值。在程序运行时,系统会根据指定的参数情况自动匹配合适的方法。
- 方法重写:是指在子类根据实际需要改写父类继承中原有的方法。方法重写会造成同一方法在不同子类中的不同表现。
方法重载虽体现了多态性,但它实际是一种静态绑定,程序在编译时决定了类对象与方法之间的一对一关系。这些方法只是具有相同的名字,实际上是不同的方法。
所以很多资料上都认为真正的动态是动态绑定的,而重载不属于多态
15 通过动态绑定实现多态
把一个方法和其所在的类或对象关联起来的方法叫做方法的绑定。
绑定分为静态绑定和动态绑定。
- 静态绑定是指程序在运行前就知道方法属于哪个类,在编译时就可直接连接到该类,定位到这个方法。
- 动态绑定是指程序在运行时根据具体的实例对象先确定是哪个方法,然后再调用这个方法
一般,动态绑定与父类和子类的向上或向下转型相关联,例如
Animal p = new Dog();//向上转型
这里的Animal是Dog的父类,Dog是Animal的子类之一。
注解:
其运行逻辑是,Java先根据p的声明类型对它进行处理。
先找到Animal父类所对应的Dog子类,再去从子类中调用对应的方法。而无法调用Dog子类没有继承到的方法
也就是说,调用的方法的实现是从子类中获取的,若子类对父类进行方法重写,那么表现的就是子类的行为。
若存在多级继承方法,而且子类对父类继承的方法进行了重写,那么系统就会从下往上逐个匹配,然后操作第一个符合条件的子类所定义的对象
// Animal.java
package Animal3;
public abstract class Animal {
abstract void shout();
}
// Dog.java
package Animal3;
public class Dog extends Animal {
@Override
void shout(){
System.out.println("Dog is shouting");
}
}
// Cat.java
package Animal3;
public class Cat extends Animal {
@Override
void shout(){
System.out.println("Cat is shouting");
}
}
// Main.java
package Animal3;
public class Main {
public static void main(String[] args){
Animal dog = new Dog();
dog.shout();
Animal cat = new Cat();
cat.shout();
}
}
多态具有三种必要条件:
- 继承
- 重写
- 向上转型
22 Java中的接口
1 接口定义
[public] interface <接口名>{
[<常量>]
[<抽象方法>]
}
创建接口的语法格式说明:
- public:修饰符。接口只能用该修饰符修饰。(默认)
- interface:关键字。接口需要使用关键字interface来声明。
- 接口名:与类名有同样的定义法则。
- 常量:由于接口需要具备公共性、最终性和静态三个特点,所以它不能声明变量
- 接口里的方法默认是
public abstract的;属性默认是public static final
2 实现接口
想要在类中实现接口,可以在声明类时使用关键字implements。其语法格式如下:
[修饰符] class <类名称> [extends <基类名称>] [implements <接口列表>]{
// 类实体
// 在类中,要实现所有接口中声明的抽象方法
}
3 实际案例
模拟电脑通过USB接口给台灯提供电源
package P10;
// 请最后再看main函数,先看下面的
public class P10 {
public static void main(String[] args){
Computer c = new Computer();//实例化computer
Light l1 = new Light();//实例化两个台灯
Light l2 = new Light();
c.usb1 = l1;//连接上[USB]接口
l1.powerReceive();//使用[USB]接口里的功能(此处为接收能量)
c.usb2 = l2;
l2.powerReceive();
System.out.println("\n-----分割线-----\n");
c.powerSupply();//整体使用USB接口
}
}
// 创建USB接口
interface USBInterface {
}
// 继承USB接口,USB接口的供能端
interface USBSupply extends USBInterface {
public void powerSupply();
}
// 继承USB接口,USB接口的接收端
interface USBReceive extends USBInterface{
public void powerReceive();
}
// 创建一个Computer电脑类
class Computer implements USBSupply{
public USBReceive usb1;//实际的接口1
public USBReceive usb2;//实际的接口2
// computer类重写powerSupply方法(供能)
// 创建这个抽象的"接口"
public void powerSupply(){
System.out.println("电脑提供能源");
usb1.powerReceive();//这个接口可以使用powerReceive接收能量的方法
usb2.powerReceive();
}
}
class Light implements USBReceive{
public void powerReceive(){
System.out.println("电灯接收能源");
}
}
运行结果:
电灯接收能源
电灯接收能源
-----分割线-----
电脑提供能源
电灯接收能源
电灯接收能源
4 接口与抽象类的区别
| 特性 | 接口(Interface) | 抽象类(Abstract Class) |
|---|---|---|
| 方法实现 | Java 8 前不能有实现(仅抽象方法),后支持默认方法 | 可以有抽象方法和具体方法 |
| 成员变量 | 默认是 public static final(常量) |
可以是任意修饰符(普通成员变量) |
| 构造方法 | 不能有 | 可以有(用于子类初始化) |
| 继承机制 | 支持多继承(一个类实现多个接口) | 单继承(一个类只能继承一个抽象类) |
| 设计目的 | 定义行为契约("能做"什么、对行为部分的抽象) | 提供通用基础实现("是什么"的抽象)(对整个类的抽象) |
| 使用场景 | 跨类别的功能扩展(如 Serializable) |
同类别的层次化抽象(如 Animal 抽象类) |
补充:
-
实现类的范围不同: 抽象类实现的类是具有相同特点的类,而接口却可以跨越不同类。
也就是说,抽象类是从子类中发现相同部分,然后泛化成抽象类,由子类继承该父类即可。
接口不同,实现它的子类可以不尊在任何关系和共同之处。例如,狮子、老虎都属于动物类,具备“叫”的行为。飞机、鸟可以有“飞”的接口,但它们是没有共同父类的,所以只能用接口实现。
因此,抽象类是一种继承的关系,而接口则不同,同样的方法可以在不同的地方可以实现完全不一样的行为 -
设计方式不同: 抽象类要先有子类,才抽象出父类,是一种从下往上的构建法则。而接口不需要先有子类,它只需要定义一些抽象方法,可以有完全不同的行为,接口是从上向下设计出来的
5 含默认方法的接口
5.1 语法格式
[public] interface <接口名>{
default [public] <返回类型> <方法名>([形参表]){
// 方法体
}
}
5.2 说明
- 带不带
public关键字,效果是一样的,通常省略 - 接口
I的子接口和实现类将自动拥有I的默认方法m,子接口和实现类也可以将默认方法m重新声明为不带方法体的抽象方法。 - 可以用
static关键字替换default关键字,来直接通过接口名调用该默认方法 - 接口可以同时含有任意个数的抽象方法和默认方法
- 若子接口同时继承的多个父接口中有同名的默认方法,则必须重写默认方法
| 特性 | 默认方法(default Method) |
静态方法(static Method) |
|---|---|---|
| 归属 | 实例方法 | 类方法 |
| 语法 | default void method() { ... } |
static void method() { ... } |
| 调用方式 | 通过实例调用:obj.method() |
通过接口名调用:Interface.method() |
| 是否可重写 | ✅ 实现类可重写 | ❌ 不可重写(隐藏而非覆盖) |
| 主要用途 | 提供接口方法的默认实现 | 提供接口相关的工具方法 |
| 访问成员变量 | 只能访问接口中的常量(static final) |
只能访问接口中的常量(static final) |
| 访问控制 | 隐式 public(不可用 private) |
支持 public/private(Java 9+) |
| 多继承冲突 | 需手动解决(Class.super.method()) |
无冲突(静态方法不参与继承) |
5.3 示例
// 默认方法.java
package Except2;
public class moren {
}
// 油车接口
interface OilCar {
// 默认方法
default void fuel(){
System.out.println("油车的燃料为汽油");
}
// 静态默认方法
static void toot(){
System.out.println("油车的喇叭声为滴滴");
}
}
// 电车接口
interface ElectricalCar {
// 同名的默认方法
default void fuel(){
System.out.println("电车通过充电来增加行驶里程");
}
// 同名的静态默认方法
static void toot(){
System.out.println("电车的喇叭声为嘟嘟");
}
}
// 油电混动车接口
interface HybridCar extends OilCar,ElectricalCar{
// 必须重写父接口中的同名默认方法
@Override
default void fuel() {
// 指定调用油车接口的默认方法
OilCar.super.fuel();
ElectricalCar.super.fuel();
System.out.println("混动车既可以用油也可以用电");
}
// 普通的默认方法
default void drive(){
System.out.println("混动车正在行驶。。。");
}
}
// BenzCar.java
package Except2;
// 奔驰混动车
public class BenzCar implements HybridCar {
public static void main(String[] args){
HybridCar c = new BenzCar();
// 调用接口的默认方法
c.fuel();
// 调用接口的静态默认方法
OilCar.toot();
ElectricalCar.toot();
}
}
注: 在 Java 接口的默认方法中,使用 InterfaceName.super.method() 的语法是为了 明确指定调用哪个父接口的默认方法,尤其是在多继承场景下出现同名默认方法冲突时。
5.4 接口的引用
5.4.1 介绍
- 在程序设计中,我们可以定义一个接口类型的引用变量来实现对接口的引用。
- 接口引用的特点:只能调用接口声明的方法。即使实际对象有其他方法,接口引用也无法直接访问。
- 为什么要使用接口引用?
-
解耦: 代码依赖接口而非具体实现,便于替换实现类。
例如
Add bb = new NumericalOperation(); // 可替换为其他 Add 的实现类 -
限制访问权限: 通过接口引用隐藏不必要的方法。
-
5.4.2 语法格式
public static void main(String[] args){
<定义的接口类型> <变量名> = <一个实现了该接口的类的实例>;
}
5.4.3 示例:
Interface1.java
package Except3;
public class Interface1 {
}
// 定义加减乘除接口
interface Add{
// 定义接口方法
int add(int a,int b);
}
interface Subtract{
int subtract(int a,int b);
}
interface Multiply{
// 定义接口方法
int multiply(int a,int b);
}
interface Divide{
int divide(int a,int b);
}
Class1.java
package Except3;
// 定义类,继承加减乘除接口
class NumericalOperation implements Add,Subtract,Multiply,Divide{
// 实现接口方法,实现加法运算
public int add(int a,int b){
return a+b;
}
// 减法
public int subtract(int a,int b){
return a-b;
}
// 乘法
public int multiply(int a,int b){
return a*b;
}
// 除法
public int divide(int a,int b){
return a/b;
}
}
Main.java(在这里定义了接口类型的引用变量)
package Except3;
public class Main {
public static void main(String[] args){
// 初始化对象
NumericalOperation aa = new NumericalOperation();
// 接口引用赋值
Add bb = aa;
Subtract cc = aa;
Multiply dd = aa;
Divide ee = aa;
// 对象引用
//可以直接访问该类中实现的所有方法(add、subtract、multiply、divide)
System.out.println("关于aa的对象引用:");
System.out.println("a+b= "+ aa.add(1, 1));
System.out.println("a-b= "+ aa.subtract(2, 3));
System.out.println("a*b= "+ aa.multiply(4, 5));
System.out.println("a/b= "+ aa.divide(6, 7));
//类型是对应的接口(如Add、Subtract等),只能访问该接口中声明的方法。
System.out.println("关于接口的对象引用:");
System.out.println("a+b= "+ bb.add(8, 9));
System.out.println("a-b= "+ cc.subtract(10, 11));
System.out.println("a*b= "+ dd.multiply(2, 10));
System.out.println("a/b= "+ ee.divide(12, 2));
}
}
注: 接口类型的引用变量(如 Add bb、Subtract cc 等)的值必须是一个实现了该接口的类的实例。
23 一个.java文件只能有一个public的顶级类/接口。
24 Java中的标准输入输出语法
Java 提供了多种方式进行标准输入输出操作,主要通过 System 类、Scanner 类和 BufferedReader 类等实现。
1 标准输出
1.1 System.out.print() 和 System.out.println()
System.out.print("Hello, "); // 不换行输出
System.out.println("World!"); // 输出并换行
// 格式化输出
int num = 10;
System.out.printf("The number is %d\n", num); // 类似C语言的printf
1.2 System.out.printf() 格式化输出
String name = "Alice";
int age = 25;
double score = 95.5;
System.out.printf("Name: %s, Age: %d, Score: %.2f\n", name, age, score);
2 标准输入
2.1 使用 Scanner 类 (最常用)
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.print("Enter a string: ");
String str = scanner.nextLine(); // 读取一行
System.out.print("Enter an integer: ");
int num = scanner.nextInt(); // 读取整数
System.out.print("Enter a double: ");
double d = scanner.nextDouble(); // 读取双精度浮点数
System.out.println("You entered: " + str + ", " + num + ", " + d);
scanner.close(); // 关闭Scanner
}
}
2.2 使用 BufferedReader 类 (效率更高)
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
System.out.print("Enter your name: ");
String name = reader.readLine();
System.out.print("Enter your age: ");
int age = Integer.parseInt(reader.readLine());
System.out.println("Hello " + name + ", you are " + age + " years old.");
reader.close();
}
}
2.3 使用 Console 类 (适用于命令行环境)
import java.io.Console;
public class Main {
public static void main(String[] args) {
Console console = System.console();
if (console == null) {
System.err.println("No console available");
return;
}
String name = console.readLine("Enter your name: ");
char[] password = console.readPassword("Enter your password: ");
console.printf("Hello, %s\n", name);
}
}
3 注意事项
Scanner类适合简单的输入操作,但要注意处理输入类型不匹配的情况BufferedReader效率更高,适合大量输入的情况Console类不能用于IDE中的控制台,只能在真正的命令行环境中使用- 使用完输入流后应该关闭它们以释放资源
- 对于数值输入,可能需要处理
NumberFormatException异常
4 示例:综合使用
import java.util.Scanner;
public class InputOutputExample {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 输入
System.out.print("Enter your name: ");
String name = scanner.nextLine();
System.out.print("Enter your age: ");
int age = scanner.nextInt();
System.out.print("Enter your height (in meters): ");
double height = scanner.nextDouble();
// 输出
System.out.println("\nUser Information:");
System.out.printf("Name: %s\n", name);
System.out.printf("Age: %d years\n", age);
System.out.printf("Height: %.2f meters\n", height);
scanner.close();
}
}
25 Java中的异常处理机制
1 try...catch...finally异常处理
语法格式如下:
try(...){
// 代码块
// 发生异常时执行catch的代码块
}catch(error1){
// 发生错误类型1时执行该代码块
}catch(error2){
// 发生错误类型2时执行该代码块
}finally{
// 无论是否发生异常,最终都处理的代码块
}
catch捕获所有异常(不区分类型)
try {
// 可能抛出异常的代码
} catch (Exception e) {
// 捕获所有 Exception 及其子类
}
2 throw语句和throws语句异常反馈
throw语句和throws语句可以直接将异常抛给调用者
2.1 throw语句
在Java中,throw语句可以抛出程序运行时的异常。但其抛出类型必须是Throwable子类类型或Throwable类的对象。获得Throwable对象的方法有两种:
第一,在catch语句中通过参数或new关键字进行方法对象的创建。
第二,若没有与之相匹配的catch语句,就检查上层try语句块,以此类推
2.1.1 throw语句的用法,具体代码如下:
package Except4;
public class Main {
public static void main(String[] args){
try{
throwException();
}catch(Exception e){
System.out.println(e.getMessage());
}
}
private static void throwException() throws Exception{
System.out.println("方法内执行");
throw new ArithmeticException("抛出异常");
}
}
运行结果:
方法内执行
抛出异常
在示例中,方法throwException()依据程序运行逻辑主动抛出ArithmeticException异常,并由该方法的调用程序块将异常抛出
2.2 throws语句
当一个方法抛出了一个异常,但并不知道如何处理时,就需要方法的调用者对异常进行处理,这时就用到了throws语句。throws语句列举了一个方法导致的所有异常类型。若一个方法抛出了异常类,可以使异常对象从调用栈向后传播,直到有适合的方法捕获它。
2.2.1 语法格式如下:
返回值类型 方法名(参数) throws 异常类型1,异常类型2(...)
2.2.2 throws语句的用法:
package Except5;
public class Main {
private static int divide(int m,int n){
return m/n;
}
public static void main(String[] args){
try{
int result = divide(3, 0);
System.out.println(result);
}catch(Exception e){
e.printStackTrace();
}
}
}
运行异常后的结果
java.lang.ArithmeticException: / by zero
at Except5.Main.divide(Main.java:5)
at Except5.Main.main(Main.java:10)
3.3 自定义异常
在实际开发中,自定义异常类可以不使用方法返回值,而使用异常代表错误。
3.3.1 语法格式
class 自定义异常类名 extends 父异常类名{
类体;
}
3.3.2 创建自定义异常类并使用
Error.java
package Except6;
public class Error extends Exception {
private int num;
// 自定义异常类构造方法
public Error(int num){
this.num = num;
}
@Override
public String toString(){
return "DefinedException {num=" + num + "}";
}
}
Main.java
package Except6;
public class Main {
public static void customThrows(int num) throws Error{
System.out.println("对"+num+"进行操作");
if(num > 100){
// 抛出自定义异常类Error
throw new Error(num);
}
System.out.println("执行该算法正常退出");
}
public static void main(String[] args){
try{
customThrows(20);
customThrows(101);
// 捕获异常并处理自定义异常
}catch(Error e){
System.out.println("捕获异常:"+e.getMessage());
}
}
}
运行结果:
对20进行操作
执行该算法正常退出
对101进行操作
捕获异常:null
26 输入/输出流与文件
1 流的概述
数据在计算机各部件之间进行传输,人们将这种情况称为“流”(Stream)。数据流有多种划分方法,从方向而言可以分为字节流与字符流。但不管是哪种数据流,其形式均为二进制,不过字符流中的数据多出了一个字符的编码和解码环节。
2 字节流与字符流的区别
输入和输出数据是计算机程序的重要工能,比如从文件或键盘中读取数据,或者把数据向文件进行传输等。在Java中,数据的这类传输过程称为“流”,用统一的接口表示。程序可以借助这种数据传输过程来访问不同的输入和输出设备。
java的流式输入和输出以四个抽象类为基础,即InputStream类、OutputStream类、Reader类和Writer类。InputStream类和OutputStream类为字节流设计,Reader类和Writer类为字符流设计。字节流与字符流形成分离的层次结构。
字节流,顾名思义,其基本处理单位是字节,而字符流的基本处理单位是16位的Unicode表示的字符。字节流和字符流的用处有所不同,字节流一般用于处理字节或二进制对象,字符流一般用于处理字符或字符串。
3 字节流
3.1 字节输入流
InputStream类为所有字节输入流的父类,也就是说,它定义了各种Java字节输入流所具有的共性。InputStream类继承层次结构如下:
- InputStream
- PipeInputStream
- FileInputStream
- ObjedInputStream
- FilterInputStream
- DataInputStream
- ButteredInputStream
- PushbackInputStream
- LineNumberInputStream
- ByteArrayInputStream
- StringBufferInputStream
字节输入流常用子类如表:
| 子类 | 说明 |
|---|---|
| DataInputStream | 从底层输入流中读取Java基本数据类型 |
| ByteArrayInputStream | 从输入流读取的数据保存在字节数组缓冲区中 |
| FileInputStream | 从文件中读取数据 |
| PrintStream | 方便其他输出流打印各种数据值表示方式 |
| PipedInputStream | 通过管道读取数据 |
| BufferedInputStream | 创建缓冲区读取数据 |
| FilterInputStream | 实现InputStream接口的过滤器输入流 |
InputStream类定义的方法如表:
| 方法 | 说明 |
|---|---|
| int available() | 返回当前可读的输入字节数 |
| void close() | 关闭输入源 |
| void mark(int numBytes) | 在输入流的当前点放置一个标记 |
| int read() | 如果下一个字节可读,则返回一个整型,遇到文件尾时返回-1 |
| int read(byte buffer[]) | 试图读取buffer.length个字节到buffer中,并返回实际成功读取的字节数,遇到文件尾时返回-1 |
| int read(byte buffer[],int offset,int numBytes) | 试图读取buffer中从buffer[offset]开始的numBytes个字节,返回实际读取的字节数,遇到文件尾时返回-1 |
| void reset() | 在先前设置的标志处重新设置输入指针 |
| long skip(long numBytes) | 忽略numBytes个输入字节,返回实际忽略的字节数 |
InputStream类常用方法如下:
- int read():读取一个字节的数据,将读到的字节数返回。若到达文件末尾则返回-1
- int read(byte buffer[ ]):把数据读入字节数组,并返回实际读到的字节数
- int read(byte buffer[ ],int offset,int numBytes):将数据读入字节数组,参数offset表示数组的偏移位置,即第一个字节应放在哪个位置;参数numBytes表示读取的最大字节数
3.2 字节输出流
OutputStream类为所有字节输出流的父类。继承结构层次如下:
| 子类 | 说明 |
|---|---|
FileOutputStream |
将数据写入文件。 |
ByteArrayOutputStream |
将数据写入内存中的字节数组(byte[])。 |
PipedOutputStream |
与 PipedInputStream 配合,用于线程间通信(管道流)。 |
FilterOutputStream |
提供增强功能的装饰器基类,常见子类包括: |
↳ BufferedOutputStream |
添加缓冲功能,提高 I/O 性能。 |
↳ DataOutputStream |
支持写入基本数据类型(如 int, double)。 |
↳ PrintStream |
提供 print()/println() 方法(如 System.out)。 |
ObjectOutputStream |
用于序列化对象(将对象写入流)。 |
DeflaterOutputStream |
压缩数据的基类,子类包括: |
ZipOutputStream |
写入 ZIP 格式的压缩文件。 |
GZIPOutputStream |
写入 GZIP 格式的压缩数据。 |
CheckedOutputStream |
计算写入数据的校验和(如 CRC32)。 |
CipherOutputStream |
对数据进行加密/解密操作。 |
OutputStream类定义的方法如表:
| 方法 | 说明 |
|---|---|
| void close() | 关闭输出流 |
| void flush() | 刷新输出缓冲区 |
| void write(int b) | 向输出流写入单个字节 |
| void write(byte buffer[ ]) | 向一个输入流写一个完整的字节数组 |
| void write(byte buffer[ ],int offset,int numBytes) | 参数offset表示数组的偏移位置,即写入位置;参数numBytes表示要写入字节的数量 |
4 字符流
4.1 字符输入流
Reader类为所有字符输入流的父类。结构层次如下:
| 子类 | 说明 | 典型用法 |
|---|---|---|
CharArrayReader |
从内存中的 char[] 读取数据 |
new CharArrayReader(new char[]{'a','b'}) |
StringReader |
从 String 对象读取数据 |
new StringReader("text") |
PipedReader |
线程间通信的管道流 | 配合 PipedWriter 使用 |
InputStreamReader |
字节流到字符流的桥梁 | 可指定字符编码 |
FileReader |
InputStreamReader 的子类,专用于文件 |
使用系统默认编码 |
BufferedReader |
添加缓冲功能 | readLine() |
LineNumberReader |
跟踪行号 | getLineNumber() |
PushbackReader |
支持回退字符 | unread() |
Reader类定义的方法如表所示:
| 方法签名 | 说明 |
|---|---|
int read() |
读取单个字符,返回字符的Unicode值(0-65535),流末尾返回-1 |
int read(char[] cbuf) |
读取字符到数组,返回实际读取的字符数,流末尾返回-1 |
int read(char[] cbuf, int off, int len) |
读取字符到数组的指定区间,off为起始偏移量,len为最大读取长度 |
long skip(long n) |
跳过并丢弃n个字符,返回实际跳过的字符数 |
boolean ready() |
判断此流是否已准备好被读取(非阻塞) |
void mark(int readAheadLimit) |
在当前位置做标记,readAheadLimit表示后续可读取的字符数限制 |
void reset() |
重置到最近一次mark()的位置 |
boolean markSupported() |
判断此流是否支持mark/reset操作 |
void close() |
关闭流并释放相关系统资源 |
transferTo(Writer out) |
将所有字符从当前Reader传输到指定的Writer(Java 10引入) |
4.2 字符输出流
Writer类为所有字符输出流的父类。结构层次如下
| 子类 | 描述 | 典型用法 | 重要特性 |
|---|---|---|---|
CharArrayWriter |
内存字符数组写入 | new CharArrayWriter() |
可转换为 char[] 或 String |
StringWriter |
字符串缓冲区写入 | new StringWriter() |
内部使用 StringBuffer |
PipedWriter |
线程间管道通信 | 配合 PipedReader 使用 |
必须连接后才能使用 |
FileWriter |
文件字符输出 | new FileWriter("file.txt") |
继承自 OutputStreamWriter |
OutputStreamWriter |
字节→字符转换 | 可指定编码 | 显式指定编码时使用 |
FileWriter |
文件专用适配器 | 使用平台默认编码 | 简单文件写入 |
BufferedWriter |
添加缓冲 | write(), newLine() |
必须包装其他 Writer |
PrintWriter |
格式化输出 | print(), printf() |
适合文本格式化 |
FilterWriter |
过滤基类 | 需子类实现 | 自定义过滤逻辑 |
Writer类定义的方法如下
| 方法签名 | 说明 | 示例 |
|---|---|---|
void write(int c) |
写入单个字符(Unicode值) | writer.write('A'); |
void write(char[] cbuf) |
写入整个字符数组 | writer.write(new char[]{'H','i'}); |
void write(char[] cbuf, int off, int len) |
写入数组的指定区间 | writer.write(buf, 0, 5); |
void write(String str) |
写入整个字符串 | writer.write("Hello"); |
void write(String str, int off, int len) |
写入字符串子串 | writer.write("World", 1, 3); |
void flush() |
强制刷新缓冲区 | writer.flush(); |
void close() |
关闭流并释放资源 | try(writer){...} |
Java 10+新增方法
| 方法签名 | 说明 | 示例 |
|---|---|---|
Writer append(CharSequence csq) |
追加字符序列 | writer.append("end"); |
Writer append(CharSequence csq, int start, int end) |
追加子序列 | writer.append("text",1,3); |
Writer append(char c) |
追加单个字符 | writer.append('!'); |
5 文件流
5.1 文件字节流
文件字节流是指文件字节输入流FileInputStream类和文件字节输出流FileOutputStream类。可以完成对本地磁盘文件的顺序输入和输出操作
FileInputStream类的构造方法
| 构造方法 | 说明 |
|---|---|
| FileInputStream(String f) | 根据参数指定的文件名创建文件字节输入流。如果文件不存在,就会抛出FileNotFoundException异常 |
| FileInputStream(File f) | 和第一个方法类似,只是参数为File类型 |
FileOutputStream类的构造方法
| 构造方法 | 说明 |
|---|---|
| FileOutputStream(String f) | 根据参数指定的文件名创建文件字节输出流。不管指定的文件是否已经存在,都会新建一个空文件 |
| FileOutputStream(String f,boolean b) | 参数2如果是false,那么功能就和第一个方法相同。否则,当参数1指定的文件已经存在时。后面的写入操作将从已有的文件末尾开始,也就是追加写入 |
| FileOutputStream(File f,boolean b) | 和第二个方法相似,只是参数1为FIle类型 |
复制文本文件的数据示例
package Except7;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileReaderTest {
// 读取文件信息
public static void main(String[] args){
// 创建文件输入流对象
FileInputStream fin = null;
// 创建文件输出流对象
FileOutputStream fout = null;
try{
// 将输入流与源文件关联
fin = new FileInputStream("Except7\\file.txt");
// 将输出流与目标文件关联,此时文件尚不存在
fout = new FileOutputStream("Except7\\file_copy.txt");
// 创建byte类型的数组b,包含128个字节
byte[] b = new byte[128];
int len = 0;
while((len = fin.read(b)) != -1){
fout.write(b,0,len);
}
System.out.println("文件复制完成!");
}catch(IOException ex){
System.out.println("文件复制失败" + ex.toString());
}finally{
try{
if(fin != null){
// 若fin对象尚未关闭
fin.close();
}
}catch(IOException ex){
System.out.println("文件输出流关闭失败: " + ex.toString());
}
try{
if(fout != null){
fout.close();
}
}catch(IOException ex){
System.out.println("文件输出流关闭失败: " + ex.toString());
}
}
}
}
说明: FileInputStream 类与 FileOutputStream 类的主要处理对象为二进制文件(如图像、视频等),不太适合用来处理文本文件。
例如,在实际应用中经常借助字节流相关类实现对二进制文件(如.exe文件或.dll文件等)的扫描,来判断文件中是否包含有某个特征码(按某种特定顺序组成的二进制编码),从而得出结论即该文件是否包含有某种病毒。
5.2 文件字符流
FileReader类和FileWriter类分别为Reader类与Writer类的子类。前者为文件字符输入流,后者为文件字符输出流
FileReader类的构造方法
| 构造方法 | 说明 |
|---|---|
| FileReader(String f) | 读取文件中的数据 |
| FileReader(File f) | 与第一个方法相似 |
FileWriter类的构造方法
| 构造方法 | 说明 |
|---|---|
| FileWriter(String f) | 把数据保存到文件中 |
| FileWriter(File f) | 与第一个方法相似 |
说明:
在Java程序中,一个汉字占用两个字节的存储空间,使用字节流也可以完成字符读取,但这种情况下可能出现乱码,所以最好用字符流来读取字符。
read()方法为FileReader类最常用的方法,这个方法的作用是读取字符文件中的字符,并返回一个int类型的数据,表示read()实际读取到的字符对应的ASCII码。若读取到文件末尾或没有读取到字符,则返回-1
writer()方法是FileWriter类最常用的方法,作用是把字符或字符串写到文件中。
读取文件示例:
package Except7;
import java.io.FileReader;
import java.io.IOException;
public class FileReader2 {
public static void main(String[] args){
FileReader fopen = null;
try{
fopen = new FileReader("Except7\\file.txt");
int readChar = fopen.read();
while(readChar != -1){
// 输出读到的内容
System.out.printf("%c",readChar);
// 读取下一个字符
readChar = fopen.read();
}
// 关闭文件阅读器
fopen.close();
}catch(IOException ex){
System.out.println("文件读取失败");
}
}
}
写入文件示例
package Example7;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class Main3 {
public static void main(String[] args){
try{
Path path = Paths.get("Example7\\file.txt");
// 拿到原始文件的内容
String content = Files.readString(path);
// 使用FIleWriter类打开新文件
FileWriter fileWriter = new FileWriter("Example7\\new_file.txt");
// 把内容写入到新文件
fileWriter.write(content);
fileWriter.write("123");
System.out.println("文件写入完成");
fileWriter.close();
}catch(IOException ex){
System.out.println("文件打开失败");
}
}
}
6 缓冲流
java提供的缓冲字节流和缓冲字符流均带有缓冲功能,其作用在于增强数据读写的效率。其原理是,这些类的内部建有一个缓冲数据的数组,读写数据时将数据存储到缓冲区,而非直接写入所连接的流中,待缓冲区存储满后或者关闭流时,再一次性把缓冲区的数据写入,从而减少读写请求的次数,尽可能增强数据读写率。
6.1 缓冲字节流
Java语言可提供具有缓冲作用的字节流,也就是BufferedInputStream类和BufferedOutputStream类。前者为缓冲字节流,用于对输入流进行缓冲;后者为缓冲字节输出流,用于对输出流进行缓冲。之所以要增加缓冲功能,是为了增加读写速度,从而提升读写效率。
BufferedInputStream和BufferedOutputStream都需要借助内部缓冲区数组来实现。
BufferedInputStream类构造方法
| 构造方法 | 说明 |
|---|---|
BufferedInputStream(InputStream in) |
使用默认缓冲区大小(通常为8192字节) |
BufferedInputStream(InputStream in, int size) |
指定缓冲区大小 |
常用方法
int read()- 读取一个字节int read(byte[] b, int off, int len)- 读取多个字节到数组的指定位置long skip(long n)- 跳过指定数量的字节int available()- 返回可读取的字节数void mark(int readlimit)- 标记当前位置void reset()- 重置到上次标记的位置boolean markSupported()- 是否支持标记功能(总是返回true)void close()- 关闭流
BufferedOutputStream类构造方法
| 构造方法 | 说明 |
|---|---|
BufferedOutputStream(OutputStream out) |
使用默认缓冲区大小(通常为8192字节) |
BufferedOutputStream(OutputStream out, int size) |
指定缓冲区大小 |
常用方法
void write(int b)- 写入一个字节void write(byte[] b, int off, int len)- 写入字节数组的指定部分void flush()- 强制将缓冲区内容写入底层流void close()- 关闭流(会先调用flush())
使用示例
- 使用BufferedInputStream和BufferedOutputStream复制文件
// 使用BufferedInputStream和BufferedOutputStream复制文件
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("input.txt"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("output.txt"))) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = bis.read(buffer)) != -1) {
bos.write(buffer, 0, bytesRead);
}
bos.flush(); // 确保所有缓冲数据被写出
} catch (IOException e) {
e.printStackTrace();
}
- 用缓冲字节流读写文件
package Example8;
import java.io.*;
public class Main1 {
public static void main(String[] args){
try{
// 文件输入流传入源文件路径
FileInputStream fin = new FileInputStream("Example8\\file.txt");
// 文件输出流传入目的文件路径
FileOutputStream fout = new FileOutputStream("Example8\\new_file.txt");
// 缓冲输入流
BufferedInputStream bin = new BufferedInputStream(fin);
// 缓冲输出流
BufferedOutputStream bout = new BufferedOutputStream(fout);
// 接收数据的字节数组
byte[] b = new byte[64];
while(bin.read(b) != -1){
// 将缓冲区的数据全部写出
bout.write(b);
}
// 刷新缓冲区到输出流
bout.flush();
// 关闭缓冲输出流
bout.close();
// 关闭输出流
fout.close();
// 关闭缓冲输入流
bin.close();
// 关闭输入流
fin.close();
}catch(ArrayIndexOutOfBoundsException e){
System.out.println("数组越界异常:" + e.getMessage());
}catch(IOException e){
System.out.println("数据流处理异常:" + e.getMessage());
}
}
}
注意事项
- 缓冲区大小选择:应根据具体应用场景选择合适的缓冲区大小,通常8KB是一个合理的默认值
- 及时关闭流:使用try-with-resources确保流被正确关闭
- 手动flush:在需要确保数据立即写入时调用flush()
- 性能考虑:对于大文件操作,缓冲流通常比非缓冲流性能更好
- 组合使用:可以与其他装饰流(如DataInputStream/ObjectInputStream)组合使用
6.2 缓冲字符流
BufferedReader类为缓冲字符输入流,其缓冲功能以传入的字符输入流为对象。
BufferedWriter类为缓冲字符输出流,其缓冲功能以传入的字符输出流为对象
缓冲原理与缓冲字节流相似
BufferedReader类的构造方法如下:
| 构造方法 | 说明 |
|---|---|
BufferedReader(InputStream inputStream) |
使用默认缓冲区大小(通常为8192字节) |
BufferedReader(InputStream inputStream, int size) |
指定缓冲区大小 |
说明:
创建BufferedReader对象后,在读取文本数据时可以调用该对象中的read()方法和readLine()方法。这两种方法中,前者可以完成对部分数据的读取,后者可以完成对整行数据的读取。
readLine()方法的特点是每次读取到回车符(\n)时结束读取,下一次读取则从回车符后开始。
使用示例:
package Example8;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class Main2 {
public static void main(String[] args){
try{
BufferedReader bufferedReader = new BufferedReader(new FileReader("Example8\\file.txt"));
// 创建line变量用于存储从文件中读取的第一行数据
String line = bufferedReader.readLine();
// 判断line变量是否接收到数据
while(line != null){
// 打印接收到的数据
System.out.println(line);
// 读取下一行文本
line = bufferedReader.readLine();
}
// 关闭流
bufferedReader.close();
}catch(IOException ex){
System.out.println("文件流出现错误:"+ex);
}
}
}
上述代码创建了一个BufferedReader对象读取文件,在碰到回车符(\n)时不会将其作为数据。读出来的每一行内容都要另外添加\n,才能保证输出的内容和文件的内容一样。
使用BufferedReader类对象从键盘输入流中读取字符
package Example8;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class Main3 {
public static void main(String[] args){
char oneByte;
int r;
try{
// 将从BufferedReader输入流中读取的信息赋值给变量oneByte
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
while ((r = bufferedReader.read()) != -1) {
oneByte = (char)r;
System.out.println(oneByte);
}
bufferedReader.close();
}catch(IOException ex){
System.out.println("流传输出现错误:"+ex);
}
}
}
从键盘读取输入的字符串
package Example8;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class Main3 {
public static void main(String[] args){
char oneByte;
String string;
int r;
try{
// 将从BufferedReader输入流中读取的信息赋值给变量oneByte
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
while ((string = bufferedReader.readLine()) != null && !string.equals("quit")) {
System.out.println(string);
}
}catch(IOException ex){
System.out.println("流传输出现错误:"+ex);
}
}
}
BufferedWriter类构造方法
| 构造方法 | 说明 |
|---|---|
BufferedWriter(OutputStream outputStream) |
使用默认缓冲区大小(通常为8192字节) |
BufferedWriter(OutputStream outputStream, int size) |
指定缓冲区大小 |
BufferedWriter对象创建完成后,可以调用BufferedWriter对象的newLine()方法来写入一个回车符。回车符的表达方法随操作系统的不同而不同,体现了Java的跨平台性。
使用BufferedWriter类将字符串写入文件
package Example8;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
public class Main4 {
public static void main(String[] args){
try{
BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter("Example8\\new_file2.txt"));
// 将字符串"畅畅今天暴富!"写入文件
bufferedWriter.write("畅畅今天暴富!");
// 换行
bufferedWriter.newLine();
bufferedWriter.close();
}catch(IOException ex){
System.out.println("流打开出现错误:"+ex);
}
}
}
7 转换流
我们在进行内容的输入和输出时往往需要使用字节流或字符流,不过有些时候需要将字符流和字节流进行转换,这就要用到转换流类的操作。InputStreamReader类和OutputStreamWriter类便具有这种转换功能。
InputStreamReader类为Reader类的子类,作用是把字节输入流转换为字符输入流,并可指定字符编码
OutputStreamWriter类为Writer类的子类,作用是把字符输出流转换为字节输出流
InputStreamReader类的构造方法如下
| 构造方法 | 说明 |
|---|---|
| InputStreamReader(InputStream in) | 使用默认字符集创建一个InputStreamReader对象 |
| InputStreamReader(InputStream in,Charset cs) | 使用给定的字符集创建一个InputStreamReader对象 |
| InputStreamReader(InputStream in,CharsetDecoder dec) | 使用给定的字符集解码创建一个InputStreamReader对象 |
| InputStreamReader(InputStream in,String charsetName) | 使用指定的字符集创建一个InputStreamReader对象 |
以上构造方法在读字节时使用指定的字符集,并把所读字节解码为字符。我们在挑选字符集时,既能够用名字指定字符集,也可以使用平台默认的字符集。charsetName参数为字符串表示的字符编码名称,如ISO 8859-1、UTF-8、UTF-16等;其中参数in是字节输入流对象;cs参数为使用的字符集CharSet对象;dec参数是用来在字节和Unicode字符之间转换的charset、解码器和编码器。
OutputStreamWriter类的构造方法如下:
| 构造方法 | 说明 |
|---|---|
| OutputStreamWriter(OutputStream out) | 使用默认的字符编码创建一个OutputStreamWriter类 |
| OutputStreamWriter(OutputStream out,Charset cs) | 使用给定的字符集创建一个OutputStreamWriter类 |
| OutputStreamWriter(OutputStream out,CharsetEncoder enc) | 使用给定的字符集编码创建一个OutputStreamWriter类 |
| OutputStreamWriter(OutputStream out,String charsetName) | 使用指定的字符集创建一个OutputStreamWriter对象 |
OutputStreamWriter类的常用方法为String getEncoding(),返回输出流正在使用的字符编码名称。
示例:
package Example8;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
public class Main5 {
public static void main(String[] args){
try{
BufferedReader bReader = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bWriter = new BufferedWriter(new FileWriter("Example8\\new_file3.txt"));
String line = null;
while((line = bReader.readLine()) != null && !line.equals("quit")){
bWriter.write(line + "\n");
bWriter.flush();
}
// 关闭
bWriter.close();
bReader.close();
}catch(IOException ex){
System.out.println("流处理出现错误:"+ex);
}
}
}
说明:在上述例子中,用系统默认的字符把标准输入流System.in这个字节流编码包装成字符流,写入文件中。
27 Path类
1 作用
Path 是一个接口(由 Paths 工具类创建实例),用于抽象地表示文件系统中的路径。它可以指向:
-
文件(如 example.txt)
-
目录(如 C:\Users\ 或 /home/user/)
-
符号链接(Symbolic Link)
2 常用方法(与File类进行搭配)
// 创建Path对象
Path path1 = Paths.get("/home/user/file.txt");
Path path2 = Path.of("/home/user/file.txt"); // Java 11+
// 获取路径信息
String fileName = path1.getFileName().toString(); // "file.txt"
Path parent = path1.getParent(); // "/home/user"
int nameCount = path1.getNameCount(); // 3
// 路径操作
Path resolved = path1.resolve("subdir"); // "/home/user/file.txt/subdir"
Path sibling = path1.resolveSibling("other.txt"); // "/home/user/other.txt"
Path relativized = path1.relativize(Paths.get("/home")); // "../.."
// 路径转换
File file = path1.toFile(); // 转换为File对象
URI uri = path1.toUri(); // 转换为URI
// 路径检查
boolean exists = Files.exists(path1);
boolean isDir = Files.isDirectory(path1);
28 Files类 文件操作
java.nio.file.Files是Java NIO包中的一个实用工具类,提供了大量静态方法来操作文件系统中的文件和目录。它是Java 7引入的,比传统的java.io.File类功能更强大、更灵活。
1 Files类的主要作用
- 文件操作:创建、删除、复制、移动文件
- 目录操作:创建、遍历、删除目录
- 文件属性:读取和设置文件属性
- 文件内容:读写文件内容
- 文件系统工具:检查文件存在性、获取文件类型等
2 常见用法
2.1 检查文件/目录是否存在
Path path = Paths.get("example.txt");
boolean exists = Files.exists(path);
boolean notExists = Files.notExists(path);
2.2 创建文件/目录
// 创建文件(如果不存在)
Path newFile = Files.createFile(Paths.get("newfile.txt"));
// 创建目录(如果不存在)
Path newDir = Files.createDirectory(Paths.get("newdir"));
// 创建多级目录
Path newDirs = Files.createDirectories(Paths.get("parent/child/grandchild"));
2.3 删除文件/目录
// 删除文件
Files.delete(Paths.get("fileToDelete.txt"));
// 安全删除(文件不存在不会抛出异常)
Files.deleteIfExists(Paths.get("fileMayNotExist.txt"));
2.4 复制文件
Path source = Paths.get("source.txt");
Path target = Paths.get("target.txt");
// 基本复制
Files.copy(source, target);
// 带选项的复制(覆盖已存在文件)
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
// 保留文件属性
Files.copy(source, target, StandardCopyOption.COPY_ATTRIBUTES);
2.5 移动/重命名文件
Path source = Paths.get("oldname.txt");
Path target = Paths.get("newname.txt");
// 基本移动
Files.move(source, target);
// 强制覆盖已存在文件
Files.move(source, target, StandardCopyOption.REPLACE_EXISTING);
// 原子移动(保证操作完整性)
Files.move(source, target, StandardCopyOption.ATOMIC_MOVE);
2.6 读取文件内容
// 读取所有字节
byte[] bytes = Files.readAllBytes(Paths.get("file.txt"));
// 读取所有行为字符串列表
List<String> lines = Files.readAllLines(Paths.get("file.txt"));
// 按行读取(大文件推荐使用)
try (Stream<String> stream = Files.lines(Paths.get("largeFile.txt"))) {
stream.forEach(System.out::println);
}
2.7 写入文件内容
// 写入字节
Files.write(Paths.get("file.txt"), "content".getBytes());
// 写入字符串行
List<String> lines = Arrays.asList("line1", "line2", "line3");
Files.write(Paths.get("file.txt"), lines);
// 追加内容
Files.write(Paths.get("file.txt"), "append".getBytes(),
StandardOpenOption.APPEND);
2.8 获取文件属性
Path path = Paths.get("example.txt");
// 文件大小
long size = Files.size(path);
// 最后修改时间
FileTime lastModified = Files.getLastModifiedTime(path);
// 是否为目录
boolean isDir = Files.isDirectory(path);
// 是否为常规文件
boolean isFile = Files.isRegularFile(path);
// 是否可读/可写/可执行
boolean readable = Files.isReadable(path);
boolean writable = Files.isWritable(path);
boolean executable = Files.isExecutable(path);
2.9 遍历目录
// 列出目录内容
try (Stream<Path> stream = Files.list(Paths.get("directory"))) {
stream.forEach(System.out::println);
}
// 递归遍历目录
try (Stream<Path> stream = Files.walk(Paths.get("directory"))) {
stream.forEach(System.out::println);
}
// 查找文件
try (Stream<Path> stream = Files.find(
Paths.get("directory"),
Integer.MAX_VALUE,
(path, attrs) -> path.toString().endsWith(".java"))) {
stream.forEach(System.out::println);
}
2.10 临时文件操作
// 创建临时文件
Path tempFile = Files.createTempFile("prefix", ".suffix");
// 创建临时目录
Path tempDir = Files.createTempDirectory("prefix");
3 注意事项
- 异常处理:大多数Files方法会抛出
IOException,需要适当处理 - 资源管理:使用
try-with-resources管理打开的流 - 性能考虑:对于大文件,避免使用
readAllBytes()和readAllLines() - 符号链接:Files类默认跟随符号链接,可以通过
LinkOption.NOFOLLOW_LINKS改变行为
29 多线程
1. 线程简介
1.1 线程的概述
同进程的中的多个线程可以共享所拥有的所有资源。一个线程拥有创建或取消了另一个线程的能力,同进程内的多个线程可以同时运行。
优点:线程相对于进程,其资源消耗更少,这使得线程在处理大量任务或需要高并发的情况下表现出优越的性能
缺点:一个线程的崩溃可能会对整个程序的稳定性产生影响,这是由于线程间的相互依赖性和共享资源;线程与主程序共享同一地址空间,这限制了可用的最大内存地址;线程之间的同步和锁定控制较为复杂,稍有不慎就可能导致数据不一致或其他问题。
1.2 进程的概述
一般的操作系统都支持进程的概念,每个运行的任务都对应一个进程。当一个程序进入内存并开始运行时,它便成为一个进程。进程是正在运行的程序,是系统管理和调度的单独单位,也是操作系统结构的基础。
进程具有以下三个显著的特点:
- 并发性: 是进程的重要特点,也是操作系统的重要特点。进程的并发执行可以更有效地利用系统资源。单个处理器上可以并发执行多个进程,且这些进程之间不会互相影响。
- 独立性: 进程是系统中的独立实体,拥有自己的资源,并且每个进程都有自己私有的地址空间。未经进程本身允许,一个用户进程不能直接访问其他进程的地址空间。
- 动态性: 进程是程序的动态执行,从创建到终止,具有生命周期。动态性是进程最基本的特点。
注: 特别强调的是,并行性和并发性是两个截然不同的概念。
并行性指的是在同一个时间点上,多个处理器上的多条指令可以同时执行。
并发性则是指在同一时间点上只能有一条指令执行,但是多个进程的指令被快速地轮换执行,从而在整体上呈现出多个进程同时执行的效果。
大部分操作系统都提供了支持多进程并发运行的功能。例如,在撰写文档的同时还可以使用软件播放音乐。此外,每台计算机在运行时还有大量的底层支撑程序在运行,这些进程看起来像是在同时工作。然而,对于单个处理器来说,在某一时刻只能执行一个程序。为了实现各个进程的平稳运行,就需要在这些程序之间不断地进行轮换执行。
2. 创建线程
2.1 使用Thread类创建
单线程程序
// Main1.java
package Example9;
public class Main1 {
public static void main(String[] args){
Threader threader = new Threader();
threader.run();
int counter = 0;
while(counter++ < 15){
System.out.println("main Thread 第 "+ counter + " 次运行");
}
System.out.println("main Thread 执行完毕");
}
}
// Threader.java
package Example9;
public class Threader extends Thread{
@Override
public void run(){
int counter = 0;
while(counter++ < 10){
System.out.printf("Threader类的run()方法第 %d 次运行\n",counter);
}
}
}
多线程程序
// Main1.java
package Example10;
public class Main1 {
public static void main(String[] args){
Threader threader = new Threader();
threader.start();
int counter = 0;
while(counter++ < 15){
System.out.printf("main Thread 第 %d 次运行\n",counter);
}
System.out.println("main Thread执行完毕");
}
}
// Threader.java
package Example10;
public class Threader extends Thread{
@Override
public void run(){
int counter = 0;
while(counter++ < 10){
System.out.printf("Threader类的run()方法第 %d 次运行\n",counter);
}
}
}
start()方法可以创建一个新的线程
Thread类包括8个构造方法,其中4个是比较常用的:
| 构造方法 | 说明 |
|---|---|
Thread() |
没有参数的构造方法 |
Thread(Runnable target) |
参数为实现Runnable接口类对象的构造函数 |
Thread(String name) |
参数为 String 型的字符串,传递线程的名称 |
Thread(Runnable target,String name) |
参数为实现Runnable 接口类对象和线程名 |
2.2 使用Runnable接口创建
在上例中,Threader类直接继承于Thread类,从而使自己变成多线程类。然而这种做法具有很大的局限性,Java是单继承的语言,一旦继承Thread类后就不能再继承其他类了。因此,除了选择直接继承Thread 外,Java还提供了另一种创建多线程对象的方式,那就是实现 Runnable 接口。
Runnable接口通常只有一个run()方法。所以,如果我们要创建一个多线程类,只需要实现这个接口,并将一个实现了该接口的对象实例传递给构造方法,这样就能创建一个多线程。
// Main.java
package Example11;
public class Main {
public static void main(String[] args){
Thread thread = new Thread(new Runner());
thread.start();
int counter = 0;
while(counter++ < 15){
System.out.println("main Thread 第 "+counter+"次运行");
}
System.out.println("main Thread执行完成!");
}
}
// Runner.java
package Example11;
public class Runner implements Runnable {
@Override
public void run(){
int counter = 0;
while (counter++ < 10) {
System.out.println("Runner类的run()方法第 "+counter+" 次运行");
}
}
}
3. 线程的生命周期
线程的生命周期包含四种独特的状态:开始(等待)、运行、挂起和停止。
这些状态可以通过Thread类中的方法进行控制。
3.1 创建并运行线程
线程在创建后并不会立即执行run()方法中的代码,而是进入等待状态。在等待状态下,线程可以通过Thread类的方法设置线程的各种属性,例如:线程的类型(setDaemon)和线程的优先级(setPriority)、线程名(setName)等。
当调用start()方法后,线程开始执行run()方法中的代码,从而进入运行状态。这时判断线程是否处于运行状态的方法是通过Thread类的isAlive()方法。当线程处于运行状态时,isAlive()返回true;线程处于等待状态或停止状态时,isAlive()返回false。
示例1:
// LifeCycle.java
package Example12;
import javax.management.RuntimeErrorException;
public class LifeCycle extends Thread{
@Override
public void run(){
int m = 0;
while((++m)<20){
try{
Thread.sleep(10);
}catch(InterruptedException e){
throw new RuntimeException(e);
}
}
}
}
// Main.java
package Example12;
public class Main {
public static void main(String[] args) throws Exception{
LifeCycle thread = new LifeCycle();
System.out.println("isAlive: "+thread.isAlive());
thread.start();
System.out.println("isAlive: "+thread.isAlive());
thread.join();//等线程thread结束后再继续执行
System.out.println("thread已经结束!");
System.out.println("isAlive: "+thread.isAlive());
}
}
上述代码中join()方法的作用是保证线程的run()方法完成后程序才继续运行
示例2:
package Example13;
public class MyThread extends Thread{
@Override
public void run(){
// 如果m小于10就循环递增
for(int m=0;m<5;m++){
System.out.println(getName() + "=" + m);
}
}
public static void main(String[] args){
for(int i=0;i<5;i++){
// 打印线程名
System.out.println(Thread.currentThread().getName() + "--" + i);
// 如果i等于2,则通过下面的代码开两条新的线程
if(i==2){
new MyThread().start();
new MyThread().start();
}
}
}
}
3.2 挂起和唤醒线程
只要线程开始执行run()方法,就会一直到run()方法执行完成线程,它才会终止。在线程执行的过程中,可以通过suspend()和sleep()两个方法让线程暂时停止执行。
当使用suspend挂起线程后,可以运用resume()方法启动线程。
当使用sleep让线程进入休眠状态时,只能在设定的时间过后才能使线程变为就绪状态。
示例:
public class ThreadControlExample {
public static void main(String[] args) {
// 创建并启动可控制线程
ControlledThread controlledThread = new ControlledThread();
controlledThread.start();
// 主线程控制controlledThread的行为
try {
// 让controlledThread运行2秒
Thread.sleep(2000);
// 挂起线程
System.out.println("\n主线程: 挂起controlledThread");
controlledThread.suspend();
// 主线程休眠1秒,观察效果
Thread.sleep(1000);
// 恢复线程
System.out.println("主线程: 恢复controlledThread");
controlledThread.resume();
// 让controlledThread运行2秒
Thread.sleep(2000);
// 让controlledThread休眠3秒
System.out.println("\n主线程: 让controlledThread休眠3秒");
controlledThread.triggerSleep(3000);
// 等待controlledThread完成
controlledThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程: 示例结束");
}
}
class ControlledThread extends Thread {
private boolean shouldSleep = false;
private long sleepTime = 0;
@Override
public void run() {
try {
for (int i = 1; i <= 10; i++) {
System.out.println("ControlledThread: 执行第 " + i + " 次");
// 检查是否需要休眠
synchronized (this) {
if (shouldSleep) {
System.out.println("ControlledThread: 进入休眠 " + sleepTime + " 毫秒");
Thread.sleep(sleepTime);
shouldSleep = false;
System.out.println("ControlledThread: 从休眠中唤醒");
}
}
// 每次循环间隔500毫秒
Thread.sleep(500);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 触发线程休眠的方法
public synchronized void triggerSleep(long time) {
this.shouldSleep = true;
this.sleepTime = time;
}
}
注:suspend()和resume()方法已经被标记为@Deprecated(不推荐使用),因为它们容易导致死锁。建议用更安全的标志变量作为替代
在线程运行时可以通过标志变量的状态来决定是否继续执行、暂停、或终止。
- 标志变量必须声明为
volatile,以保证一个线程的修改对其他线程可见
private volatile boolean running = true;
private volatile boolean paused = false;
- 控制方法的实现
// 停止线程
public void stopThread() {
running = false;
}
// 暂停线程
public void pauseThread() {
paused = true;
}
// 恢复线程
public void resumeThread() {
paused = false;
}
- 完整示例代码
public class FlagControlledThread extends Thread {
// 控制标志
private volatile boolean running = true; // 控制线程是否继续运行
private volatile boolean paused = false; // 控制线程是否暂停
@Override
public void run() {
int count = 0;
while (running) {
// 检查是否暂停
if (paused) {
System.out.println("线程暂停中...");
try {
Thread.sleep(200); // 暂停时降低检查频率
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
continue; // 跳过本次循环剩余部分
}
// 正常工作代码
System.out.println("正在工作: " + (++count));
try {
Thread.sleep(500); // 模拟工作耗时
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
System.out.println("线程已停止");
}
// 控制方法
public void stopThread() {
running = false;
}
public void pauseThread() {
paused = true;
}
public void resumeThread() {
paused = false;
}
public static void main(String[] args) throws InterruptedException {
FlagControlledThread worker = new FlagControlledThread();
worker.start();
// 主线程控制worker线程
Thread.sleep(2000);
System.out.println("\n主线程: 暂停工作线程");
worker.pauseThread();
Thread.sleep(2000);
System.out.println("主线程: 恢复工作线程");
worker.resumeThread();
Thread.sleep(2000);
System.out.println("\n主线程: 停止工作线程");
worker.stopThread();
worker.join(); // 等待工作线程结束
System.out.println("程序结束");
}
}
3.3 线程阻塞
线程在执行过程中肯呢个需要暂停,以使其他线程有机会进行执行。在计算机系统中,当发生以下五种情况,线程将出现阻塞状态:
- 线程调用了一个阻塞式I/O方法,在该方法返回之前,该线程将一直被阻塞
- 线程主动放弃占用的处理器资源,调用了
sleep()方法。 - 线程视图获取一个被其他线程持有的同步监视器。
- 程序调用了线程的
suspend()方法将该线程挂起。此方法容易导致死锁,因此不建议使用 - 线程在等待某个通知(notify)
当执行的线程出现阻塞情况后,其他线程就有机会执行了。被阻塞的线程会在恰当的实际重新进入就绪状态。值得注意的是,被阻塞的线程在阻塞解除后不会立即进入运行状态,而是要等待线程调度器再次调度它。
3.4 线程死亡
线程有三种方式可以技术,一旦结束,线程将进入死亡状态。
- 其一是采用
run()方法正常完成执行 - 其二是抛出未捕获的
Exception或Error - 其三是直接调用线程的
stop()方法来结束该线程,但这种方法可能导致死锁,因此不到万不得已最好不用
为了检测一个线程是否已经死亡,可以调用线程对象中的isAlive()方法。当线程处于就绪、运行、或阻塞状态时,该方法将返回true;而当线程处于新建、死亡状态时,该方法将返回false。
不要尝试使用start()方法再次启动已经死亡的线程,因为死亡的线程将无法再次作为线程执行
package Example13;
public class DeadThread extends Thread {
@Override
public void run(){
for(int i=0;i<5;i++){
// 当线程类继承Thread类时,可以直接调用getName方法返回当前进程名
System.out.println("thread-"+getName()+"-"+i);
}
}
public static void main(String[] args){
// 创建线程对象
DeadThread thread = new DeadThread();
for(int i=0;i<20;i++){
// 调用Thread的currentThread方法获取当前线程
System.out.println(Thread.currentThread().getName()+ "-"+i);
if(i==10){
// 启动线程
thread.start();
// 判断启动后线程的isAlive()值,输出应该为true
System.out.println(thread.isAlive());
}
}
// 只有当线程处于新建、死亡两种状态时,isAlive方法返回false
// 此时i>20,线程不处于新建状态,只可能是死亡状态
System.out.println(thread.isAlive());
if(!thread.isAlive()){
// 试图再次启动线程
thread.start();
}
}
}
在上述代码中,当线程已经死亡时,再次调用start()方法来启动该线程会导致异常,这表明死亡状态的线程无法再次运行。
currentThread() 是 Thread 类的静态方法,用于获取当前正在执行的线程对象的引用。它的作用是返回代码当前所在的线程。
4. 线程的调度和优先级
4.1 线程的调度
在程序的繁杂线路中,众多线程是同时执行的,但是某个线程若想真正运行,就必须获得CPU使用权。Java虚拟机依照特定的规则为每个线程分配CPU使用权,此规则被称为线程调度。
在计算机的世界里,线程调度分为分时调度模型和抢占式调度模型。
分时调度模型就像我们轮流使用一个物品,每个线程轮流获得CPU的使用权。
而抢占式调度模式则更像是优先级比赛,可使池中优先级高的线程抢先占用CPU。而对于优先级相同的线程,随机选择一个线程使其占用CPU,一旦失去CPU的使用权后,再由随机选择的线程获取CPU的使用权。
Java虚拟机默认采用抢占式调度模型,所有Java虚拟机都确保了在不同优先级之间的抢占式线程调度的使用。当高优先级的线程准备运行时,如果低优先级的线程正在运行,Java虚拟机会适时(可能是立即)暂停低优先级的线程,让高优先级的线程运行。此时,高优先级的线程就成功抢占(preempt)了低优先级线程的CPU运行权。
4.2 线程的优先级
Java的线程的优先级可以用1~10的证书表示,其中10是最高优先级,1是最低优先级。在多线程环境下,Java虚拟机通常会优先运行优先级最高的线程,但这并不是绝对的规则。一般情况下,创建的线程默认优先级为5,除非特意设置,否则都会采用这个默认值。
Java的1、5、10三个优先级由三个命名常量表示,如表所示
| 常量 | 说明 |
|---|---|
| static int MIN_PRIORITY | 最低优先级,值为1 |
| static int NORM_PRIORITY | 默认优先级,值为5 |
| static int MAX_PRIORITY | 最高优先级,值为10 |
在程序的运行过程中,各个处于就绪状态的线程都有其各自的优先级,而没有通过setPriority()方法设定优先级的线程,其默认的优先级都是5。我们可以通过getPriority()方法来获取线程当前的优先级。
代码示例:
package Example14;
// ThreadPriorityDemo.java
public class ThreadPriorityDemo {
public static void main(String[] args){
Thread minPriority = new Thread(new MyRunnable(),"较低优先级的线程");
Thread maxPriority = new Thread(new MyRunnable(), "较高优先级的线程");
// 设置线程的优先级为MIN_PRIORITY = 1
minPriority.setPriority(Thread.MIN_PRIORITY);
// 设置线程的优先级为MAX_PRIORITY = 10
maxPriority.setPriority(Thread.MAX_PRIORITY);
// 启动线程
minPriority.start();
maxPriority.start();
}
}
// MyRunnable.java
package Example14;
public class MyRunnable implements Runnable{
@Override
public void run(){
for(int i=0;i<3;i++){
System.out.println(Thread.currentThread().getName() + "正在输出" + i);
}
}
}
运行结果:
较高优先级的线程正在输出0
较低优先级的线程正在输出0
较高优先级的线程正在输出1
较低优先级的线程正在输出1
较高优先级的线程正在输出2
较低优先级的线程正在输出2
在上述代码中,虽然我们设置了优先级顺序,但Java线程优先级是对调度器的提示,而不是强制命令。操作系统可以自由选择忽略这些提示。
且不同操作系统对于Java的优先级的处理也不同。
操作系统差异:不同操作系统处理线程优先级的方式不同:
-
Windows有7个优先级级别,会映射Java的10个级别
-
Linux使用完全不同的调度算法,基本忽略Java优先级
-
macOS也有自己的调度方式
因此,在设计多线程应用程序时,我们不应该依赖线程优先级来控制线程的执行顺序,而应将线程优先级看作是提高程序效率的一种方法。
5. 线程同步
对于多线程操作的安全问题,Java提供了对应的解决方案,即同步机制。这种机制的内容是,数据无法同时被两个或两个以上的线程访问,如果数据已经被一个线程访问,则其他线程无法同时对该数据进行访问,直到之前的进程访问结束。
同步最常见的方式就是使用锁(Lock),也称为线程锁。锁为非强制机制,当一个线程想要访问数据或资源是,需要先尝试获取(Acquire)锁,完成访问后则需要释放(Release)锁。如果获取锁时锁已被占用,线程则会进入等待状态,直到锁被释放再次变为可用
5.1 同步方法
同步方法的做法是在方法前加synchronized关键字来修饰某个方法。每一个用synchronized关键字声明的方法都是临界区。
用此关键字修饰方法时,内置锁会保护整个方法,其原因在于Java的每个对象都有一个互斥锁。如果没有获取互斥锁,则调用该方法时会处于阻塞状态,此状态会持续到获得互斥锁为止。当同步方法执行结束后互斥锁会被释放,此时其他等待中的线程方可竞争。
使用synchronized关键字的语法格式如下
public synchronized 返回值数据类型 方法名([参数列表]){
// 方法体语句
}
代码示例:
package Example15;
// TicketWindowThread.java
public class TicketWindowThread implements Runnable{
// 火车票总数
private static int totalTicketCount = 10;
@Override
public synchronized void run(){
// 在run方法前加上关键字synchronized,使run()方法体的内容形成临界区,同一时刻只能被一个线程访问
while(true){
if(totalTicketCount>0){
System.out.println(Thread.currentThread().getName() + "售出第 " + (10-totalTicketCount+1)+" 张票。还剩" + (totalTicketCount-1)+"张");
totalTicketCount--;
Thread.yield();
}else{
break;
}
}
}
}
// TicketWindowDemo.java
package Example15;
public class TicketWindowDemo {
public static void main(String[] args){
TicketWindowThread window = new TicketWindowThread();
Thread t1 = new Thread(window,"第一个售票口");
Thread t2 = new Thread(window, "第二个售票口");
Thread t3 = new Thread(window,"第三个售票口");
t1.start();
t2.start();
t3.start();
}
}
运行结果:
第一个售票口售出第 1 张票。还剩9张
第一个售票口售出第 2 张票。还剩8张
第一个售票口售出第 3 张票。还剩7张
第一个售票口售出第 4 张票。还剩6张
第一个售票口售出第 5 张票。还剩5张
第一个售票口售出第 6 张票。还剩4张
第一个售票口售出第 7 张票。还剩3张
第一个售票口售出第 8 张票。还剩2张
第一个售票口售出第 9 张票。还剩1张
第一个售票口售出第 10 张票。还剩0张
上述程序中通过增加synchronized关键字将run()方法变成了同步方法,从而锁定了run()的主体,不能有两个或两个以上的线程同时访问,每张票只卖出了一次。不过结果和我们预想的有所差别,由于每次都只有获得互斥锁的窗口在售卖,所以争端程序无异于单线程在运行。
没能实现每个窗口都售票分析:
以上程序是在run()方法中加synchronized关键字,run()方法的内容就是实现售票,直至票全部售出。我们可以发现,不应该在run()方法上加synchronized关键字,而应该在售票的过程中加该关键字(即totalTicketCount),这样,收票结束后就会释放锁。不过要实现该功能,需要借助同步代码块。
5.2 同步代码块
同步代码块是由synchronized关键字修饰的语句块。这类语句块被该关键字修饰后会自动加上互斥锁,从而具有同步的特点。其语法格式如下:
synchronized(object){
// 同步代码块语句
}
需要注意的是,同步的特点是高开销,因而我们应使同步的内容尽量精简。一般而言,无需将整个方法同步,借助synchronized代码块将关键代码同步就可以。
代码示例:
// TicketWindowThread.java
package Example16;
public class TicketWindowThread extends Thread{
// 火车票总数
private static int totalTicketCount = 12;
@Override
public void run(){
while(true){
if(totalTicketCount > 0){
// 加锁,synchronized关键字修饰同步代码块,某时刻只能被一个线程访问
synchronized(this){
System.out.printf("%s售出第%d张票。还剩%d张\n",Thread.currentThread().getName(),12-totalTicketCount+1,totalTicketCount-1);
totalTicketCount--;
}
}else{
break;
}
}
}
}
// TicketWindowDemo.java
package Example16;
public class TicketWindowDemo {
public static void main(String[] args){
TicketWindowThread window = new TicketWindowThread();
Thread t1 = new Thread(window,"售票口1号");
Thread t2 = new Thread(window,"售票口2号");
Thread t3 = new Thread(window, "售票口3号");
t1.start();
t2.start();
t3.start();
}
}
运行结果
售票口1号售出第1张票。还剩11张
售票口1号售出第2张票。还剩10张
售票口1号售出第3张票。还剩9张
售票口1号售出第4张票。还剩8张
售票口1号售出第5张票。还剩7张
售票口1号售出第6张票。还剩6张
售票口1号售出第7张票。还剩5张
售票口1号售出第8张票。还剩4张
售票口1号售出第9张票。还剩3张
售票口1号售出第10张票。还剩2张
售票口1号售出第11张票。还剩1张
售票口1号售出第12张票。还剩0张
售票口3号售出第13张票。还剩-1张
售票口2号售出第14张票。还剩-2张
当一个线程发出请求后,会先检查totalTicketCount是否大于0,若不是则直接返回。这样一来就节省了因进入synchronized块而耗费的资源。不过从结果而言仍然是错误的。下面以A、B两个线程为例,分析问题无法解决掉原因
- A、B线程同时进入了第一个if判断,
totalTicketCount大于0 - A、B同时进入了
synchronized代码块,若A先获得互斥锁,则A执行售票并输出售票结果。A执行完totalTicketCount--后离开了synchronized代码块,将互斥锁释放。B获得互斥锁,也执行相同的操作,输出售票结果后离开(最后也会执行totalTicketCount--)
综上所述,可优化代码:
// TicketWindowThread.java(优化)
package Example16;
public class TicketWindowThread extends Thread{
// 火车票总数
private static int totalTicketCount = 12;
@Override
public void run(){
while(true){
// 第一次检查
if(totalTicketCount > 0){
// 加锁,synchronized关键字修饰同步代码块,某时刻只能被一个线程访问
synchronized(this){
// 新增第二次检查
if(totalTicketCount>0){
System.out.printf("%s售出第%d张票。还剩%d张\n",Thread.currentThread().getName(),12-totalTicketCount+1,totalTicketCount-1);
totalTicketCount--;
}
}
}else{
break;
}
}
}
}
// TicketWindowDemo.java
package Example16;
public class TicketWindowDemo {
public static void main(String[] args){
TicketWindowThread window = new TicketWindowThread();
Thread t1 = new Thread(window,"售票口1号");
Thread t2 = new Thread(window,"售票口2号");
Thread t3 = new Thread(window, "售票口3号");
t1.start();
t2.start();
t3.start();
}
}
执行结果:
售票口1号售出第1张票。还剩11张
售票口1号售出第2张票。还剩10张
售票口1号售出第3张票。还剩9张
售票口1号售出第4张票。还剩8张
售票口1号售出第5张票。还剩7张
售票口1号售出第6张票。还剩6张
售票口1号售出第7张票。还剩5张
售票口1号售出第8张票。还剩4张
售票口1号售出第9张票。还剩3张
售票口1号售出第10张票。还剩2张
售票口1号售出第11张票。还剩1张
售票口1号售出第12张票。还剩0张
双重检查锁(double checking locking) 是指,在加锁(synchronized)之前,首先进行一轮检查,加锁之后再进行一次检查。
双重检查锁的作用是,当多个线程同时通过了第一次检查时,能保证第二次检查时只有其中一个线程通过,使其后进入第二次检查的线程均产生互斥。这样一来,如果火车票已经售空,便无法继续售票。
30 Lambda函数
基本语法:
(参数列表) -> { 方法体 }
-
如果方法体只有一行,可以省略
{}和return -
如果参数只有一个,可以省略
()
31 方法引用
方法引用是 Lambda 的语法糖,当 Lambda 只是“把参数原封不动地传给某个方法”时,可以用 :: 来简化。
示例:
1. 对象的实例方法引用
形式:
对象::方法
等价写法:
x -> 对象.方法(x)
例子:
list.forEach(e -> System.out.println(e));
list.forEach(System.out::println); // 简化
2. 类的静态方法引用
形式:
类名::静态方法
等价写法:
(x, y) -> 类名.静态方法(x, y)
例子:
list.sort((a, b) -> Integer.compare(a, b));
list.sort(Integer::compare); // 简化
3. 类的实例方法引用
形式:
类名::实例方法
等价写法:
(x, y) -> x.实例方法(y)
例子:
list.sort((s1, s2) -> s1.compareTo(s2));
list.sort(String::compareTo); // 简化
4. 构造方法引用
形式:
类名::new
等价写法:
x -> new 类名(x)
例子:
list.stream().map(s -> new User(s));
list.stream().map(User::new); // 简化
32 Java 泛型(Generics)
泛型!
常用约定:
| 名字 | 意义 |
|---|---|
| T | Type(类型) |
| E | Element(元素) |
| K | Key(键) |
| V | Value(值) |
该命名只是约定俗成,没有强制要求
1. 什么是泛型?
泛型就是:给类、接口或方法定义一个“占位符类型”,在使用时再确定具体是什么类型。
作用:
- 代码复用:同一套逻辑可以处理多种类型
- 类型安全:编译器能检查类型是否正确,避免强制类型转换错误
2. 经典示例:集合中的泛型
没有泛型时
List list = new ArrayList();
list.add("hello");
list.add(123); // 也能加,但取出来要强转
String s = (String) list.get(0); // 需要强制转换
使用泛型
List<String> list = new ArrayList<>();
list.add("hello");
// list.add(123); // ❌ 报错,类型不对
String s = list.get(0); // 不需要强转
3. 泛型类
class Box<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
使用:
Box<String> box1 = new Box<>();
box1.set("hello");
System.out.println(box1.get()); // hello
Box<Integer> box2 = new Box<>();
box2.set(123);
System.out.println(box2.get()); // 123
4. 泛型方法
public <T> void print(T value) {
System.out.println(value);
}
调用:
print("abc"); // T 为 String
print(123); // T 为 Integer
print(3.14); // T 为 Double
5. 泛型接口
interface Converter<F, T> {
T convert(F from);
}
class StringToIntConverter implements Converter<String, Integer> {
@Override
public Integer convert(String from) {
return Integer.parseInt(from);
}
}
使用:
Converter<String, Integer> c = new StringToIntConverter();
System.out.println(c.convert("123")); // 输出 123
6.泛型的省略
- 为什么可以省略泛型?
在 Java
中,当创建对象时,若编译器已经能根据上下文推断出泛型类型,就可以使用
菱形语法 <> 来省略类型。
例如:
Result<String> r = Result.success("Hello");
编译器已经确定 E = String
因此下面写法是完全合法的:
return new Result<>(0, "操作成功", data);
等同于:
return new Result<E>(0, "操作成功", data);
- 示例代码
public class Result<T> {
private T data;
public Result(T data) {
this.data = data;
}
public static <E> Result<E> success(E data) {
return new Result<>(data); // 省略了 <E>
}
public static void main(String[] args) {
Result<String> res = Result.success("OK");
System.out.println("结果:" + res.data);
}
}
📌 总结
- 泛型就是类型参数化:逻辑写一次,类型自由替换
- 优点:
- 代码复用
- 类型安全
- 常见形式:
- 泛型类(
Box<T>) - 泛型接口(
Converter<F, T>) - 泛型方法(
<T> void print(T value)) - 集合类(
List<String>、Map<K, V>)
- 泛型类(
使用泛型的另一种场景:
泛型T就是没有规定具体类型,可以使返回值格外灵活
public class Result<T> {
private Integer code;//业务状态码 0-成功 1-失败
private String message;//提示信息
private T data;//响应数据
//快速返回操作成功响应结果(带响应数据)
public static <E> Result<E> success(E data) {
return new Result<>(0, "操作成功", data);
}
//快速返回操作成功响应结果
public static Result success() {
return new Result(0, "操作成功", null);
}
public static Result error(String message) {
return new Result(1, message, null);
}
}
方法是否需要定义泛型取决于“是否返回具体类型的数据”
-
✅
success(E data)有泛型-
因为要返回
data -
数据类型不确定,需要泛型承载
-
-
❌
success()没有泛型-
不返回数据(
data = null) -
泛型没有意义
-
-
❌
error(String message)没有泛型- 错误响应通常不带业务数据
33.Jar包的制作及使用
什么是Jar包?
相当于Python的库,在Java中,你可以把自己写好的程序做成Jar包让别人可以导入使用
IDEA中通过Maven来制作Jar包(企业开发唯一推荐方式)
- 新建一个Maven项目
- (在
src/main中)写好Java代码后并完成测试 - 在右侧工具栏中选择
Maven>生存期>package并双击,Maven就会自动打包
注:
src/test目录下的代码不会被打包,仅用于测试
IDEA通过工件制作Jar包(仅个人使用,不推荐)
- 文件>项目结构>工件>
- 点中间上边的
+ - 选择Jar>从具有依赖项的模块>确定
- 选择一个合适的输出路径,并点击右下角的
应用 - 确定
- 在上方的菜单栏中选择
构建 - 构建>构建工件>构建
- Jar包就出现在你选择的目录里了
IDEA里使用Jar包
- 创建一个与
src同级的lib文件夹 - 将Jar包移动过去
- 右键该Jar包,选择添加到库
34.Java 匿名类与 Lambda 表达式完全总结
在 Java 中,为了简化代码,尤其是回调函数(Callback)和事件监听器(Listener)的编写,我们通常使用 匿名内部类 或 Lambda 表达式。以下是它们的几种主要形式。
1. 匿名内部类 (Anonymous Inner Class)
适用场景:
-
Java 8 之前的所有版本
-
需要重写的方法 超过一个(例如
MouseListener) -
需要继承一个 抽象类或普通类(例如
MouseAdapter)
形式 A:实现接口 (Interface)
Runnable myTask = new Runnable() {
@Override
public void run() {
System.out.println("运行在匿名内部类中");
}
};
new Thread(myTask).start();
形式 B:继承类 (Class / Abstract Class)
⚠️ 注意:这种情况不能使用 Lambda 表达式替代
button.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
System.out.println("鼠标按下了");
}
// 其他方法由 MouseAdapter 提供空实现
});
2. Lambda 表达式 (Lambda Expression)
适用场景:
-
Java 8 及以上
-
必须是 函数式接口(只有一个抽象方法)
形式 A:完整写法(参数类型 + 花括号)
button.addActionListener((ActionEvent e) -> {
System.out.println("按钮被点击");
System.out.println("执行多行逻辑");
});
形式 B:省略参数类型(类型推断)
button.addActionListener((e) -> {
System.out.println("按钮被点击");
});
形式 C:极简写法(省略括号和花括号)
button.addActionListener(e -> System.out.println("按钮被点击"));
3. 方法引用 (Method Reference)
适用场景:
- Lambda 体中仅调用一个已存在的方法
// Lambda 写法
list.forEach(item -> System.out.println(item));
// 方法引用写法
list.forEach(System.out::println);
4. 总结与对比
| 特性 | 匿名内部类 | Lambda 表达式 |
|---|---|---|
| 引入版本 | Java 1.1 | Java 8 |
| 核心本质 | 创建一个匿名类对象 | 行为的函数式表达 |
| 是否生成 class 文件 | 是(如 $1.class) | 否(JVM 动态处理) |
| 适用范围 | 接口 / 抽象类 / 普通类 | 仅函数式接口 |
| 可实现方法数量 | 多个 | 仅一个 |
| 是否可继承类 | 可以 | 不可以 |
| 是否可定义字段 | 可以 | 不可以 |
| this 指向 | 匿名类自身 | 外部类 |
一句话总结
匿名内部类 = 有状态的对象实现
Lambda 表达式 = 无状态的行为描述
Java 注解的三步骤实现机制(总结版)
从底层实现视角理解 Java 注解,而不是只停留在“会用”。
35.在Java中注解的实现
一、步骤一:定义一个注解式接口
Java 中的注解,本质上是一个特殊的接口,使用 @interface 定义。
示例(概念代码)
public @interface MyAnnotation {
// 注解属性(本质是接口中的方法)
String value();
int count() default 1;
}
要点说明
- 使用
@interface定义注解 - 注解中的方法:
- 没有参数
- 没有方法体
- 返回值就是注解属性的类型
default用于指定注解属性的默认值
二、步骤二:定义类 / 接口,并使用(调用)注解
注解本身不会执行任何逻辑,它只是用于标记元数据。
示例(概念代码)
@MyAnnotation(value = "class-level", count = 2)
public class DemoService {
@MyAnnotation("method-level")
public void doSomething() {
System.out.println("doing...");
}
}
本质说明
- 注解信息会在编译期写入
.class文件 - JVM 不会主动执行注解
- 注解是否能在运行期被读取,取决于保留策略
示例命令(概念说明):
@Retention(RetentionPolicy.RUNTIME)
只有使用 RUNTIME,注解才能通过反射获取。
三、步骤三:JVM 通过匿名类 / 动态代理实现注解并返回值
程序员从未手动创建注解对象,却可以直接调用:
annotation.value()
示例:通过反射读取注解(概念代码)
import java.lang.reflect.Method;
public class Test {
public static void main(String[] args) throws Exception {
Class
MyAnnotation classAnno = clazz.getAnnotation(MyAnnotation.class);
System.out.println(classAnno.value());
System.out.println(classAnno.count());
Method method = clazz.getMethod("doSomething");
MyAnnotation methodAnno = method.getAnnotation(MyAnnotation.class);
System.out.println(methodAnno.value());
System.out.println(methodAnno.count());
}
}
底层原理说明
- JVM 在运行期会:
- 动态生成一个匿名类
- 该匿名类实现注解接口
- 接口中的每个方法返回从
.class文件中解析出的值
等价理解(概念模型)
class MyAnnotationImpl implements MyAnnotation {
public String value() {
return "class-level";
}
public int count() {
return 2;
}
}
⚠️ 注意:
该类并不存在于源码中,仅用于理解 JVM 的行为。
四、三步骤一句话总结
Java 注解 = 接口定义 + 注解标记 + JVM 动态实现
- 使用
@interface定义一个注解接口 - 在类 / 方法 / 字段上使用该注解进行标记
- JVM 在运行期通过动态代理或匿名类实现该接口,并返回注解属性值
五、面试标准答案
Java 注解的本质是一个接口,JVM 会在运行期通过动态代理生成该接口的实现类,并通过反射调用其方法获取注解属性值。
36.字典转json
在SpringBoot中:
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.HashMap;
import java.util.Map;
public class Test {
public static void main(String[] args) throws Exception {
Map<String, Object> map = new HashMap<>();
map.put("name", "chang");
map.put("age", 25);
map.put("online", true);
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(map);
System.out.println(json);
}
}
输出
{"name":"chang","age":25,"online":true}
Java中基本类型与封装类型的转换
1. 基本类型 → 包装类型(装箱)
| 基本类型 | 包装类型 | 写法(推荐) |
|---|---|---|
| int | Integer | Integer x = Integer.valueOf(10); |
| double | Double | Double x = Double.valueOf(10.5); |
| float | Float | Float x = Float.valueOf(10.5f); |
| long | Long | Long x = Long.valueOf(10L); |
| boolean | Boolean | Boolean x = Boolean.valueOf(true); |
| char | Character | Character x = Character.valueOf('a'); |
| byte | Byte | Byte x = Byte.valueOf((byte)1); |
| short | Short | Short x = Short.valueOf((short)1); |
👉 实际开发更常用:
Integer x = 10; // 自动装箱
2. 包装类型 → 基本类型(拆箱)
| 包装类型 | 转换方法 |
|---|---|
| Integer | intValue() |
| Double | doubleValue() |
| Float | floatValue() |
| Long | longValue() |
| Boolean | booleanValue() |
| Character | charValue() |
| Byte | byteValue() |
| Short | shortValue() |
Integer x = 10;
int y = x.intValue(); // 或自动拆箱

浙公网安备 33010602011771号