Java新手村第二站:泛型、集合与IO流初探
泛型
泛型的概念与作用:
核心目的:在编译期提供类型安全检查,避免运行时的
ClassCastException
。
ClassCastException
是一种运行时异常(属于RuntimeException
),表示试图将一个对象强制转换为不兼容的类类型时发生的错误。示例:
Object obj = new Integer(100); String str = (String) obj; // 抛出 ClassCastException
主要功能:
- 类型参数化:允许类、接口、方法操作的数据类型被参数化。
- 代码复用:通过类型抽象,编写更通用的代码。
- 类型安全:编译时检查类型一致性,减少强制类型转换。
示例:
// 无泛型 List list = new ArrayList(); list.add("Hello"); String s = (String) list.get(0); // 需显式强制转换 // 有泛型 List<String> list = new ArrayList<>(); list.add("Hello"); String s = list.get(0); // 自动类型安全
泛型类与泛型接口:
定义语法:在类/接口名后添加
<T>
,T
为类型参数(可自定义名称如E
,K
,V
)。类型参数:可以是任意非基本类型(需用包装类如
Integer
)。示例:
// 泛型类 public class Box<T> { private T content; public void setContent(T content) { this.content = content; } public T getContent() { return content; } } Box<String> stringBox = new Box<>(); stringBox.setContent("Java"); String value = stringBox.getContent(); // 无需强制转换 // 泛型接口 public interface Comparator<T> { int compare(T o1, T o2); }
泛型方法:
独立于类泛型:泛型方法的类型参数独立于类的泛型参数。
语法:在返回类型前声明
<T>
。示例:
public class Utils { public static <T> T getFirstElement(List<T> list) { return list.isEmpty() ? null : list.get(0); } } List<Integer> numbers = Arrays.asList(1, 2, 3); Integer first = Utils.getFirstElement(numbers); // 自动类型推断
通配符:
通配符(Wildcard) 是一个让程序员在不破坏类型安全的前提下,灵活处理未知类型的特殊符号。它的核心作用是放宽泛型类型约束,让泛型容器或方法能兼容更多类型,同时避免出现
ClassCastException
。
无界通配符
<?>
用途:表示“未知类型”,用于接受任意泛型类型的对象。
限制:不能添加元素(除
null
),只能读取为Object
。示例:
public void printList(List<?> list) { for (Object elem : list) { System.out.println(elem); } }
上界通配符
<? extends T>
用途:接受
T
或其子类的泛型类型。限制:只能读取为
T
,不能添加元素(除null
)。示例:
// 接受Number及其子类(如Integer、Double) public double sum(List<? extends Number> list) { double sum = 0; for (Number num : list) { sum += num.doubleValue(); } return sum; }
下界通配符
<? super T>
用途:接受
T
或其父类的泛型类型。限制:可以添加
T
及其子类对象,但读取时需强制转换。示例:
// 向集合中添加Integer及其子类 public void addNumbers(List<? super Integer> list) { for (int i = 1; i <= 5; i++) { list.add(i); } }
类型擦除:
核心机制:泛型仅在编译期存在,运行时类型参数被擦除为原始类型(Raw Type)。
规则:
- 无界类型参数(如
<T>
)→ 替换为Object
。- 有界类型参数(如
<T extends Number>
)→ 替换为边界类型(Number
)。- 泛型方法的类型参数被擦除后可能生成桥方法(Bridge Methods)。
示例:
// 编译前 public class Box<T> { private T content; public void setContent(T content) { /* ... */ } } // 编译后(类型擦除) public class Box { private Object content; public void setContent(Object content) { /* ... */ } }
注意事项:
- 无法在运行时获取泛型类型信息(如
new T()
或T.class
)。- 泛型数组的创建受限(如
new List<String>[10]
非法)。泛型与继承
- 泛型类不可协变:
List<String>
不是List<Object>
的子类。- 通配符实现协变/逆变:
List<? extends Number>
是协变的(接受List<Integer>
)。List<? super Integer>
是逆变的(接受List<Number>
)。注意事项:
基本类型不可用:泛型类型参数必须是引用类型。
// 错误 List<int> list = new ArrayList<>(); // 正确 List<Integer> list = new ArrayList<>();
无法实例化类型参数:
public class Box<T> { private T obj = new T(); // 编译错误 }
静态上下文限制:静态变量或方法不能引用类的类型参数。
public class Box<T> { private static T staticObj; // 编译错误 }
包装类
Java通过包装类为基本数据类型提供了对象表示,使得基本类型可以用于面向对象的场景。
每个基本类型都有对应的包装类:
byte
→Byte
short
→Short
int
→Integer
long
→Long
float
→Float
double
→Double
char
→Character
boolean
→Boolean
作用:
泛型支持
集合类(如
List
、Map
)只能存储对象,不能存储基本类型。例如:List<Integer> list = new ArrayList<>();
。Null 值处理
包装类可以表示null
,用于区分缺失值和默认值(如0
或false
)。实用方法
提供类型转换、进制转换等方法,例如:
Integer.parseInt("123")
:字符串转int
。Integer.toHexString(255)
:转十六进制字符串。自动装箱与拆箱
装箱:基本类型 → 包装类对象(编译器调用
valueOf()
)。Integer a = 100; // 自动装箱,等价于 Integer.valueOf(100)
拆箱:包装类对象 → 基本类型(编译器调用
xxxValue()
)。int b = a; // 自动拆箱,等价于 a.intValue()
注意事项:
- 频繁装箱可能影响性能(对象创建开销)。
- 拆箱时若对象为
null
,抛出NullPointerException
。缓存机制:
部分包装类(如
Integer
、Byte
、Short
、Long
、Character
)对特定范围的值缓存对象:
- 默认范围:
-128
到127
(可通过JVM
参数调整IntegerCache.high
)。- 效果:
Integer.valueOf(127) == Integer.valueOf(127)
返回true
,但new Integer(127)
始终创建新对象。建议:优先使用
valueOf()
而非构造函数,以利用缓存。不可变性:
包装类对象一旦创建,值不可修改。所有修改操作(如加法)会生成新对象。
Integer x = 10; x = x + 5; // 新对象赋值给 x,原对象未被修改。
常用方法:
- 类型转换:
static int parseInt(String s)
:字符串 → 基本类型。String toString()
:对象 → 字符串。- 比较:
int compareTo(Integer another)
:比较大小。boolean equals(Object obj)
:比较值是否相等(而非==
比较引用)。注意事项:
- 比较操作:
==
比较对象引用,仅在缓存范围内有效。- 建议用
equals()
或拆箱后比较基本类型。- 性能敏感场景:
- 避免在循环中频繁装箱,优先使用基本类型数组(如
int[]
)。- Null 安全:
- 拆箱前需确保包装类对象非
null
。示例代码:
// 自动装箱与拆箱 Integer num1 = 200; // 装箱(若超出缓存范围,每次新建对象) int num2 = num1; // 拆箱 // 比较问题 Integer a = 100, b = 100; System.out.println(a == b); // true(缓存范围内) Integer c = 200, d = 200; System.out.println(c == d); // false(超出缓存范围) // 实用方法 String s = "123"; int n = Integer.parseInt(s); // 字符串 → int
集合
本周只是简单的了解一下常用集合的基本使用,后续会深入学习Java集合框架
集合类型 接口 实现类 特点 典型应用场景 动态数组 List
ArrayList
随机访问快,增删慢 需要频繁按索引访问元素 链表 List
LinkedList
增删快,随机访问慢 频繁插入/删除,栈/队列操作 哈希表 Set
/Map
HashSet
/HashMap
无序,O(1) 时间查询 快速去重、键值对映射 有序树 Set
/Map
TreeSet
/TreeMap
自动排序,O(log n) 时间操作 需要有序遍历或范围查询 双端队列 Deque
ArrayDeque
高效头尾操作 栈、队列、滑动窗口 常用方法:
ArrayList
(动态数组)List<Integer> list = new ArrayList<>(); // 增 list.add(1); // 末尾添加元素 list.add(0, 10); // 在索引0插入元素 list.addAll(otherList); // 合并集合 // 删 list.remove(0); // 删除索引0的元素 list.remove(Integer.valueOf(3)); // 删除元素3 list.clear(); // 清空 // 查 int num = list.get(0); // 获取索引0的元素 int size = list.size(); // 长度 boolean isEmpty = list.isEmpty(); // 遍历 for (int val : list) { /* ... */ } list.forEach(val -> System.out.println(val));
LinkedList
(链表)LinkedList<Integer> linkedList = new LinkedList<>(); // 可当作队列或栈使用 linkedList.addFirst(1); // 头部插入 linkedList.addLast(2); // 尾部插入 int first = linkedList.pollFirst(); // 移除头部 int last = linkedList.pollLast(); // 移除尾部
HashSet
(哈希集合)Set<Integer> set = new HashSet<>(); set.add(5); // 添加元素 set.remove(5); // 删除元素 boolean exists = set.contains(5); // 存在性检查 set.size(); // 大小 // 遍历 for (int val : set) { /* ... */ }
HashMap
(哈希表)Map<String, Integer> map = new HashMap<>(); // 增/改 map.put("apple", 1); // 添加键值对 map.put("apple", 2); // 更新值 map.putIfAbsent("apple", 3); // 仅当键不存在时插入 // 删 map.remove("apple"); // 删除键 map.remove("apple", 2); // 仅当值匹配时删除 // 查 int count = map.get("apple"); // 获取值(需处理null) boolean hasKey = map.containsKey("apple"); boolean hasValue = map.containsValue(2); // 遍历 for (Map.Entry<String, Integer> entry : map.entrySet()) { String key = entry.getKey(); int value = entry.getValue(); }
TreeMap
(有序哈希表)TreeMap<Integer, String> treeMap = new TreeMap<>(); treeMap.put(3, "three"); treeMap.put(1, "one"); treeMap.put(2, "two"); // 获取第一个键(最小) int firstKey = treeMap.firstKey(); // 获取小于等于4的最大键 Integer floorKey = treeMap.floorKey(4); // 范围查询:键在[1, 3)的条目 Map<Integer, String> subMap = treeMap.subMap(1, true, 3, false);
ArrayDeque
(双端队列)Deque<Integer> deque = new ArrayDeque<>(); // 可当作栈或队列使用 deque.push(1); // 栈顶压入元素(相当于addFirst) int top = deque.pop(); // 栈顶弹出(相当于removeFirst) deque.offer(2); // 队尾添加(相当于addLast) int head = deque.poll(); // 队头弹出(相当于removeFirst)
使用技巧:
快速去重
Set<Integer> unique = new HashSet<>(list); // 直接去重 List<Integer> uniqueList = new ArrayList<>(unique);
频率统计
Map<Character, Integer> freq = new HashMap<>(); for (char c : s.toCharArray()) { freq.put(c, freq.getOrDefault(c, 0) + 1); }
排序
List<Integer> list = new ArrayList<>(Arrays.asList(3, 1, 4)); Collections.sort(list); // 升序 Collections.sort(list, (a, b) -> b - a); // 降序
优先队列(堆)
// 最小堆(默认) PriorityQueue<Integer> minHeap = new PriorityQueue<>(); // 最大堆 PriorityQueue<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a); minHeap.offer(3); minHeap.offer(1); minHeap.poll(); // 弹出1(最小值)
字符串与集合互相转换
// 字符串转字符集合 char[] chars = s.toCharArray(); List<Character> list = new ArrayList<>(); for (char c : chars) list.add(c); // 集合转数组 Integer[] arr = list.toArray(new Integer[0]);
工具类
Collections
和Arrays
:
Collections
常用方法List<Integer> list = new ArrayList<>(Arrays.asList(3, 1, 2)); Collections.reverse(list); // 反转 [2, 1, 3] Collections.swap(list, 0, 2); // 交换位置 [3, 1, 2] int max = Collections.max(list); // 最大值3 int freq = Collections.frequency(list, 2); // 元素2的出现次数
Arrays
常用方法int[] arr = {3, 1, 2}; Arrays.sort(arr); // 排序 [1, 2, 3] int index = Arrays.binarySearch(arr, 2); // 二分查找索引1 // 数组转列表(注意返回的是固定大小的列表) List<Integer> list = Arrays.asList(1, 2, 3);
选择技巧:
- 需要快速随机访问? →
ArrayList
- 需要频繁插入/删除? →
LinkedList
- 需要去重或快速存在性检查? →
HashSet
/HashMap
- 需要有序遍历? →
TreeSet
/TreeMap
- 实现栈或队列? →
ArrayDeque
IO流
什么是流(Stream)?
- 流就像一根管道:数据通过这根管道从源头(如文件、网络)传输到程序,或从程序传输到目标。
- 两种方向:
- 输入流:读取外部数据到程序(如从文件读内容)。
- 输出流:将程序数据写入外部(如向文件写内容)。
流的分类
分类依据 类型 核心类 数据单位 字节流 InputStream
/OutputStream
字符流 Reader
/Writer
数据流向 输入流 FileReader
/FileInputStream
输出流 FileWriter
/FileOutputStream
功能 节点流(直接操作数据源) FileInputStream
、FileReader
处理流(增强功能,包装节点流) BufferedInputStream
、BufferedReader
File类的使用
作用:
- 表示文件或目录的抽象路径,用于操作文件的元信息(创建、删除、重命名等)。
- 注意:File 类不能直接读写文件内容,需配合 IO 流使用。
常用方法:
File file = new File("test.txt"); // 获取文件信息 System.out.println(file.getName()); // 文件名 System.out.println(file.getPath()); // 相对路径 System.out.println(file.exists()); // 是否存在 // 创建与删除 file.createNewFile(); // 创建文件 file.delete(); // 删除文件
字节流
字节流以 8 位字节(byte,1 字节) 为基本单位处理数据,适合处理所有类型的二进制数据(如图片、视频、音频等)或原始字节流。
核心类
- 基类:
InputStream
(输入流)和OutputStream
(输出流)。- 常见实现类:
FileInputStream
/FileOutputStream
:文件读写。ByteArrayInputStream
/ByteArrayOutputStream
:内存字节数组读写。BufferedInputStream
/BufferedOutputStream
:带缓冲的字节流,提升性能。DataInputStream
/DataOutputStream
:读写基本数据类型(如int
,double
)。ObjectInputStream
/ObjectOutputStream
:对象的序列化与反序列化。示例:
// 使用字节流复制文件 try (InputStream in = new FileInputStream("source.jpg"); OutputStream out = new FileOutputStream("target.jpg")) { byte[] buffer = new byte[1024]; int bytesRead; while ((bytesRead = in.read(buffer)) != -1) { out.write(buffer, 0, bytesRead); } } catch (IOException e) { e.printStackTrace(); }
字符流
字符流以 16 位 Unicode 字符(char,2 字节) 为基本单位处理数据,适合处理文本数据(如
.txt
,.csv
文件),会自动处理字符编码问题。
核心类
- 基类:
Reader
(输入流)和Writer
(输出流)。- 常见实现类:
FileReader
/FileWriter
:文件读写(默认使用平台编码,可能乱码)。InputStreamReader
/OutputStreamWriter
:字节流与字符流的桥梁,可指定编码(如 UTF-8)。BufferedReader
/BufferedWriter
:带缓冲的字符流,提升性能。StringReader
/StringWriter
:字符串读写。示例:
// 使用字符流复制文本文件(指定编码为 UTF-8) try (Reader reader = new InputStreamReader( new FileInputStream("source.txt"), StandardCharsets.UTF_8); Writer writer = new OutputStreamWriter( new FileOutputStream("target.txt"), StandardCharsets.UTF_8)) { char[] buffer = new char[1024]; int charsRead; while ((charsRead = reader.read(buffer)) != -1) { writer.write(buffer, 0, charsRead); } } catch (IOException e) { e.printStackTrace(); } // 使用 BufferedReader 逐行读取文本 try (BufferedReader br = new BufferedReader( new FileReader("text.txt"))) { String line; while ((line = br.readLine()) != null) { System.out.println(line); } } catch (IOException e) { e.printStackTrace(); }
处理流
在 Java I/O 中,处理流(Processing Streams)(也称装饰流或包装流)是建立在基础字节流或字符流之上的高级流,用于对底层流进行功能扩展或数据加工。它们通过装饰者模式动态增强流的能力,例如添加缓冲、数据转换、对象序列化等功能。
缓冲流
作用:通过内存缓冲区减少物理I/O操作次数,提升读写效率。
类名 基类 功能 典型用途 BufferedInputStream
InputStream
为字节输入流添加缓冲区 读取文件、网络数据 BufferedOutputStream
OutputStream
为字节输出流添加缓冲区 写入文件、网络数据 BufferedReader
Reader
为字符输入流添加缓冲区 逐行读取文本文件 BufferedWriter
Writer
为字符输出流添加缓冲区 高效写入文本数据 示例:
// 使用缓冲流复制文件(字节流) try (InputStream in = new FileInputStream("source.jpg"); BufferedInputStream bis = new BufferedInputStream(in); OutputStream out = new FileOutputStream("target.jpg"); BufferedOutputStream bos = new BufferedOutputStream(out)) { byte[] buffer = new byte[1024]; int bytesRead; while ((bytesRead = bis.read(buffer)) != -1) { bos.write(buffer, 0, bytesRead); } } catch (IOException e) { e.printStackTrace(); } // 使用缓冲流逐行读取文本(字符流) try (BufferedReader br = new BufferedReader(new FileReader("text.txt"))) { String line; while ((line = br.readLine()) != null) { System.out.println(line); } } catch (IOException e) { e.printStackTrace(); }
数据转换流
作用:直接读写基本数据类型(如
int
,double
)或字符串。
类名 基类 功能 典型用途 DataInputStream
InputStream
从字节流读取基本数据类型 读取二进制数据文件 DataOutputStream
OutputStream
向字节流写入基本数据类型 写入二进制数据文件 示例:
// 写入基本数据类型到文件 try (DataOutputStream dos = new DataOutputStream( new FileOutputStream("data.bin"))) { dos.writeInt(100); // 写入 int dos.writeDouble(3.14); // 写入 double dos.writeUTF("Hello"); // 写入 UTF-8 字符串 } catch (IOException e) { e.printStackTrace(); } // 从文件读取基本数据类型 try (DataInputStream dis = new DataInputStream( new FileInputStream("data.bin"))) { int num = dis.readInt(); double value = dis.readDouble(); String text = dis.readUTF(); System.out.println(num + ", " + value + ", " + text); } catch (IOException e) { e.printStackTrace(); }
序列化流
作用:实现对象的序列化(将对象转为字节流)与反序列化(从字节流重建对象)。
类名 基类 功能 典型用途 ObjectInputStream
InputStream
反序列化对象 读取序列化对象 ObjectOutputStream
OutputStream
序列化对象 写入序列化对象 示例:
// 序列化对象到文件 class Person implements Serializable { String name; int age; // 构造方法、getter/setter 省略 } try (ObjectOutputStream oos = new ObjectOutputStream( new FileOutputStream("person.dat"))) { Person p = new Person("Alice", 30); oos.writeObject(p); } catch (IOException e) { e.printStackTrace(); } // 从文件反序列化对象 try (ObjectInputStream ois = new ObjectInputStream( new FileInputStream("person.dat"))) { Person p = (Person) ois.readObject(); System.out.println(p.getName() + ", " + p.getAge()); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); }
字符编码转换流
作用:处理字节流与字符流之间的转换,并指定字符编码。
类名 基类 功能 典型用途 InputStreamReader
Reader
将字节输入流转为字符输入流 按指定编码读取文本文件 OutputStreamWriter
Writer
将字符输出流转为字节输出流 按指定编码写入文本文件 示例:
// 按 UTF-8 编码读取文本文件 try (Reader reader = new InputStreamReader( new FileInputStream("text.txt"), StandardCharsets.UTF_8)) { char[] buffer = new char[1024]; int charsRead; while ((charsRead = reader.read(buffer)) != -1) { System.out.println(new String(buffer, 0, charsRead)); } } catch (IOException e) { e.printStackTrace(); }
打印流
作用:提供格式化的输出功能(如
print()
,println()
,printf()
)。
类名 基类 功能 典型用途 PrintStream
OutputStream
格式化输出字节流 控制台输出(System.out) PrintWriter
Writer
格式化输出字符流 写入格式化的文本数据 示例:
// 使用 PrintWriter 写入格式化的文本 try (PrintWriter pw = new PrintWriter("output.txt")) { pw.println("Hello World"); pw.printf("PI = %.2f", Math.PI); } catch (IOException e) { e.printStackTrace(); }
函数式接口和Lambda表达式
函数式接口
定义:
- 仅含一个抽象方法:接口中必须且只能有一个未实现的抽象方法。
- 允许其他方法:可以包含默认方法(
default
)、静态方法(static
)或从Object
类继承的方法(如toString()
、equals()
)。- 注解标记:建议使用
@FunctionalInterface
注解,编译器会强制校验接口是否符合规范。示例:
@FunctionalInterface interface MyFunctionalInterface { void doWork(); // 唯一的抽象方法 default void log(String msg) { // 默认方法(允许存在) System.out.println("Log: " + msg); } static void staticMethod() { // 静态方法(允许存在) System.out.println("Static method"); } }
为什么需要函数式接口?
- 为Lambda表达式提供类型:Lambda表达式本质是函数式接口的实例。
- 支持函数式编程范式:将行为(方法)作为参数传递,增强代码灵活性。
- 简化代码:避免匿名内部类的冗余语法。
@FunctionalInterface
注解的作用:
- 编译时校验:确保接口仅有一个抽象方法,不符合则报错。
- 文档提示:明确该接口设计意图是作为Lambda的类型。
注意事项:
抽象方法唯一性:即使接口继承其他接口,所有父接口的抽象方法也计入总数。
@FunctionalInterface interface Child extends MyFunctionalInterface { void anotherMethod(); // 编译错误:此时抽象方法数量为2 }
Object
类方法不计入:如果抽象方法是Object
类的方法(如equals
),不影响函数式接口的定义。@FunctionalInterface interface ValidInterface { boolean equals(Object obj); // 来自Object类,不计入抽象方法数量 void execute(); }
Lambda表达式
定义:Lambda表达式是Java8引入的语法糖,用于简化函数式接口的实现。可以使代码更简洁,避免匿名内部类的冗余语法,提升可读性。
语法:
(参数列表) -> { 代码主体 }
- 参数列表:可省略参数类型(编译器自动推断);单参数时可省略括号。
- 箭头符号
->
:分隔参数和代码主体。- 代码主体:单行代码可省略大括号和
return
;多行需用大括号。示例:
// 匿名内部类 Runnable r1 = new Runnable() { @Override public void run() { System.out.println("Hello"); } }; // Lambda表达式 Runnable r2 = () -> System.out.println("Hello"); //Lambda表达式用于集合遍历 List<String> names = Arrays.asList("Alice", "Bob"); names.forEach(name -> System.out.println(name));