Java 进阶-全面解析
目录
异常处理
集合框架
List 集合
Set 集合
Map 集合
文件与字符集
IO 流
多线程
通过继承Thread类创建线程
通过实现Runnable接口创建线程
线程同步示例
线程通信示例
网络编程
Java 高级技术
反射机制
动态代理
注解
异常处理
在 Java 编程的过程中,异常处理是保障程序稳健运行的关键环节。异常主要分为 Checked Exception(受检异常)和 Unchecked Exception(非受检异常)这两大类。
受检异常要求开发者在代码中必须显式地进行处理。以IOException为例,它常常在程序与外部资源进行交互时出现,像从文件读取数据或者建立网络连接这类操作。如果不处理IOException,程序在编译阶段就会报错,强制开发者考虑可能出现的问题并进行妥善处理。这是因为与外部资源交互时存在诸多不确定性,例如文件可能不存在、网络可能中断等,通过强制处理受检异常,能让开发者提前做好应对策略,提升程序的可靠性。
非受检异常,比如NullPointerException(空指针异常)和ArrayIndexOutOfBoundsException(数组越界异常),大多是由于编程逻辑上的失误导致的。编译器不会强制要求处理这类异常,但一旦它们在程序运行时出现,往往会致使程序崩溃。例如,当代码尝试访问一个null对象的方法或属性时,就会抛出NullPointerException。这类异常通常意味着代码中存在潜在的逻辑错误,需要开发者仔细排查和修复。
异常处理主要通过try-catch-finally块来实现。在try块中,放置的是可能会抛出异常的代码。当try块中的代码抛出异常时,程序流程会立即跳转到对应的catch块。每个catch块专门用于捕获并处理特定类型的异常。例如:
try {
FileReader fileReader = new FileReader("nonexistent.txt");
} catch (FileNotFoundException e) {
System.out.println("文件未找到,请检查路径");
} finally {
// 这里可以关闭资源,即使fileReader为null也需处理,避免空指针异常
}
在上述代码中,try块尝试创建一个FileReader对象来读取名为nonexistent.txt的文件。如果该文件不存在,就会抛出FileNotFoundException异常,此时程序会进入catch块,打印出提示信息。finally块则无论try块中是否发生异常,都会被执行。它通常用于释放资源,比如关闭文件流,确保资源的正确管理,避免资源泄漏。即使try块中由于文件不存在抛出异常,导致fileReader对象未能成功创建(即fileReader为null),在finally块中也需要进行相应的空指针检查,以确保安全地关闭可能存在的资源。
此外,开发者还可以使用throw关键字手动抛出异常,以便在特定的业务逻辑下,主动告知调用者出现了问题。例如,当某个业务规则不满足时,可以手动抛出一个自定义的异常。同时,通过throws声明方法可能抛出的异常,将异常处理的责任交给调用该方法的代码。这样,调用者在调用方法时,就需要明确知道可能会遇到的异常情况,并进行相应的处理。
集合框架
Java 的集合框架为开发者提供了一系列丰富的接口和类,用于高效地存储和操作对象。该框架主要划分为Collection和Map两大体系。
List 集合
List集合的特点是有序且允许元素重复。这意味着元素在集合中的存储顺序与它们被添加的顺序一致,并且可以存在多个相同的元素。在实际应用中,常用的List实现类有ArrayList和LinkedList。
ArrayList是基于数组实现的。数组的特性使得ArrayList在查询元素时具有很高的效率,因为可以通过元素的索引直接访问数组中的对应位置。例如:
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("apple");
arrayList.add("banana");
String element = arrayList.get(0); // 高效获取元素
arrayList.add(1, "cherry"); // 插入元素,可能导致元素移动
在上述代码中,首先创建了一个ArrayList对象,并向其中添加了两个元素"apple"和"banana"。当使用get(0)方法时,可以直接快速地获取到索引为 0 的元素"apple"。这是因为ArrayList内部维护了一个数组,通过索引可以直接定位到数组中对应位置的元素,时间复杂度为 O (1)。然而,当在列表中间位置(如索引为 1 的位置)插入新元素"cherry"时,ArrayList需要将索引 1 及之后的所有元素向后移动一个位置,以腾出空间插入新元素。如果列表中元素数量较多,这种移动操作会消耗较多的时间和资源,导致性能下降。例如,当列表中有大量元素时,每一次插入操作都可能需要移动大量元素,使得插入操作的时间复杂度变为 O (n),其中 n 为列表中元素的数量。
LinkedList则是基于链表结构实现的。链表由一系列节点组成,每个节点包含元素值以及指向下一个节点的引用(在双向链表中还包含指向前一个节点的引用)。这种结构使得LinkedList在进行插入和删除操作时表现出色。因为只需要修改相关节点的引用关系,而不需要像ArrayList那样移动大量元素。例如,在链表中删除一个元素,只需要将该元素前一个节点的next引用指向该元素的下一个节点即可。对于双向链表,还需要将后一个节点的prev引用指向该元素的前一个节点。这种操作的时间复杂度为 O (1),只涉及到对几个节点引用的修改。但在查询元素时,由于链表没有像数组那样的索引直接定位机制,需要从链表的头节点开始,依次遍历每个节点,直到找到目标元素,所以查询效率相对较低。在最坏情况下,查询一个不在链表中的元素需要遍历整个链表,时间复杂度为 O (n)。
Set 集合
Set集合的特性是无序且不允许元素重复。这意味着元素在Set中的存储顺序并非按照添加顺序,并且集合中不会出现两个相同的元素。常见的Set实现类有HashSet和TreeSet。
HashSet是基于哈希表实现的。哈希表利用哈希函数将元素映射到特定的存储位置,这使得插入和查询操作在平均情况下具有 O (1) 的时间复杂度,性能表现非常优秀。为了保证元素的唯一性,HashSet要求存储的元素必须正确重写hashCode()和equals()方法。例如:
HashSet<Integer> hashSet = new HashSet<>();
hashSet.add(1);
hashSet.add(2);
hashSet.add(1); // 重复元素,不会被添加
在这段代码中,首先创建了一个HashSet对象,并向其中添加了两个不同的整数1和2。当再次尝试添加已经存在的元素1时,HashSet会根据元素的哈希码和equals()方法来判断该元素是否已经存在于集合中。哈希函数会根据元素的值计算出一个哈希码,通过哈希码可以快速定位到哈希表中的一个位置。如果该位置已经有元素存在,就需要进一步通过equals()方法来比较两个元素是否相等。如果相等,则认为是重复元素,不会再次添加,从而保证了集合中元素的唯一性。如果元素没有正确重写hashCode()和equals()方法,可能会导致相同的元素被错误地认为是不同的元素,从而破坏集合的唯一性。
TreeSet是基于红黑树实现的。红黑树是一种自平衡的二叉搜索树,这使得TreeSet中的元素会按照一定的顺序排列。默认情况下,TreeSet会按照元素的自然顺序进行排序,例如对于整数类型,就是从小到大排序。同时,也可以通过传入自定义的Comparator接口实现,来指定特定的排序规则。例如,如果要对自定义的对象进行排序,可以实现Comparator接口,在compare()方法中定义对象之间的比较逻辑。这种有序性在需要对元素进行排序遍历的场景中非常有用,例如在统计数据、查找特定范围的元素等操作中。
Map 集合
Map集合用于存储键值对(key - value pairs),其特点是一个键最多只能映射到一个值。在实际开发中,常用的Map实现类有HashMap、TreeMap和LinkedHashMap。
HashMap基于哈希表实现,具有较高的插入和查询效率。与HashSet类似,为了保证键的唯一性,键对象需要正确重写hashCode()和equals()方法。例如:
HashMap<String, Integer> hashMap = new HashMap<>();
hashMap.put("one", 1);
hashMap.put("two", 2);
Integer value = hashMap.get("one");
在上述代码中,创建了一个HashMap对象,并向其中添加了两个键值对,键分别为"one"和"two",对应的值分别为1和2。当使用get("one")方法时,可以快速地根据键"one"获取到对应的值1。HashMap通过哈希函数将键映射到哈希表中的特定位置,插入和查询操作在平均情况下时间复杂度为 O (1)。但如果哈希函数设计不合理,导致大量键映射到同一个位置(即哈希冲突严重),会使得插入和查询操作的性能下降,时间复杂度可能会接近 O (n)。
TreeMap基于红黑树实现,它会对键进行排序。这使得在遍历TreeMap时,键值对会按照键的顺序依次输出。排序方式可以是键的自然顺序,也可以通过自定义的Comparator来指定。这种有序性在需要对键值对进行有序处理的场景中非常有用。例如,在一个存储学生成绩的TreeMap中,以学生姓名作为键,成绩作为值。如果按照学生姓名的字母顺序遍历TreeMap,可以方便地查看成绩的分布情况。
LinkedHashMap继承自HashMap,它在HashMap的基础上,额外维护了一个双向链表来记录元素的插入顺序或者访问顺序。如果希望在遍历集合时,元素的顺序与它们被插入的顺序一致,或者按照访问顺序(最近访问的元素排在前面)进行遍历,LinkedHashMap就是一个很好的选择。例如,在实现一个缓存机制时,可以使用基于访问顺序的LinkedHashMap。当缓存满了需要移除元素时,可以移除最久未被访问的元素(即链表头部的元素),从而保证缓存中始终保留最近使用过的元素。
文件与字符集
在 Java 中,File类主要用于处理文件和目录路径名。通过File类,开发者可以执行一系列操作,如创建新文件、删除文件、重命名文件或目录,以及检查文件或目录的属性。例如:
File file = new File("example.txt");
try {
if (file.createNewFile()) {
System.out.println("文件创建成功");
} else {
System.out.println("文件已存在");
}
} catch (IOException e) {
e.printStackTrace();
}
在这段代码中,首先创建了一个File对象,指向名为example.txt的文件。然后使用createNewFile()方法尝试创建该文件。如果文件成功创建,该方法会返回true,并打印出相应的成功提示信息;如果文件已经存在,则返回false,并打印出文件已存在的提示。如果在创建文件过程中出现输入输出相关的错误,如磁盘空间不足、权限不够等,就会抛出IOException异常,通过catch块捕获并打印异常堆栈信息,以便开发者定位和解决问题。例如,如果磁盘空间已满,createNewFile()方法会抛出IOException,异常信息中会包含与磁盘空间相关的错误描述,帮助开发者快速找到问题所在。
字符集在处理文本数据时起着至关重要的作用。不同的字符集定义了如何将字符编码为字节序列,以及如何将字节序列解码为字符。Java 使用Charset类来表示各种字符集,常见的字符集有 UTF - 8、GBK 等。在读取和写入文本文件时,必须指定正确的字符集,否则可能会出现乱码问题。例如:
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("file.txt"), "UTF - 8"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
在上述代码中,使用BufferedReader来读取文件内容。BufferedReader通过InputStreamReader与FileInputStream关联,并且指定了字符集为UTF - 8。这意味着在读取文件时,会按照UTF - 8字符集的规则将文件中的字节数据转换为字符数据。UTF - 8是一种广泛使用的字符集,它可以表示世界上几乎所有的字符,并且具有良好的兼容性和扩展性。如果文件实际采用的字符集与指定的UTF - 8不一致,就可能导致读取的内容出现乱码。例如,如果文件是使用 GBK 字符集编码的,而在读取时指定为UTF - 8,那么原本正确的中文字符可能会显示为乱码。在while循环中,通过readLine()方法逐行读取文件内容,并将每一行打印出来。如果在读取过程中出现输入输出错误,同样会抛出IOException异常,由catch块进行处理。
IO 流
Java 的 IO 流体系分为字节流和字符流,它们为处理输入输出操作提供了不同的方式。
字节流以字节为单位处理数据,非常适合处理二进制数据,如图片、音频、视频等文件。字节流的主要类包括InputStream和OutputStream及其子类。以FileInputStream和FileOutputStream为例:
try (FileInputStream fis = new FileInputStream("source.jpg");
FileOutputStream fos = new FileOutputStream("target.jpg")) {
int b;
while ((b = fis.read()) != -1) {
fos.write(b);
}
} catch (IOException e) {
e.printStackTrace();
}
在这段代码中,首先创建了FileInputStream对象fis,用于从source.jpg文件中读取字节数据;同时创建了FileOutputStream对象fos,用于将读取到的字节数据写入到target.jpg文件中。在while循环中,通过fis.read()方法每次读取一个字节的数据,并将其赋值给变量b。当read()方法返回-1时,表示已经读取到文件末尾。在循环内部,使用fos.write(b)方法将读取到的字节写入到目标文件中。这样就实现了文件的复制操作。如果在读取或写入过程中出现任何输入输出错误,都会抛出IOException异常,由catch块捕获并打印异常信息。例如,如果在读取过程中文件被意外删除,fis.read()方法会抛出IOException,提示文件不存在的错误。
字符流以字符为单位处理数据,更适合处理文本数据。字符流基于Reader和Writer类及其子类,如FileReader和FileWriter。字符流内部会根据指定的字符集进行字节和字符之间的转换。例如:
try (BufferedReader reader = new BufferedReader(new FileReader("file.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
多线程
多线程技术允许程序同时执行多个任务,这极大地提高了程序的响应性和资源利用率。在 Java 中,创建线程主要有两种方式:继承Thread类和实现Runnable接口。
通过继承Thread类创建线程
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " 打印: " + i);
try {
// 线程休眠100毫秒
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadExample {
public static void main(String[] args) {
MyThread thread = new MyThread();
// 启动线程
thread.start();
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " 打印: " + i);
try {
// 线程休眠100毫秒
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
在上述代码中,首先定义了一个MyThread类,它继承自Thread类。在MyThread类中重写了run()方法,这个方法是线程执行的主体。在run()方法中,通过一个for循环打印出当前线程的名称以及循环变量i的值。每次打印后,线程会调用Thread.sleep(100)方法进入休眠状态 100 毫秒,这模拟了线程执行一些耗时操作的情况。在main方法中,创建了MyThread类的实例thread,然后调用thread.start()方法启动线程。需要注意的是,不能直接调用thread.run()方法,因为这样只是在主线程中普通地调用一个方法,并不会启动一个新的线程。启动线程后,主线程也会执行一个类似的for循环,打印出主线程的相关信息。由于多线程执行的不确定性,主线程和新启动的线程的打印信息可能会交替出现。
通过实现Runnable接口创建线程
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " 打印: " + i);
try {
// 线程休眠100毫秒
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class RunnableExample {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
// 启动线程
thread.start();
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " 打印: " + i);
try {
// 线程休眠100毫秒
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
这种方式中,定义了一个MyRunnable类实现了Runnable接口,在MyRunnable类的run()方法中同样编写了线程的执行逻辑。在main方法中,首先创建了MyRunnable类的实例myRunnable,然后将这个实例作为参数传递给Thread类的构造函数,创建了一个Thread对象thread。最后调用thread.start()方法启动线程。实现Runnable接口相较于继承Thread类更具优势,因为 Java 不支持多重继承,通过实现接口可以避免继承带来的局限性,使得类可以同时实现多个接口,增强了代码的灵活性。
线程同步示例
多线程编程中,线程安全问题是需要重点关注的。当多个线程同时访问共享资源时,如果没有进行适当的同步控制,可能会导致数据不一致等问题。例如,多个线程同时对一个共享的计数器进行增加操作,可能会出现计数不准确的情况。可以使用synchronized关键字来实现线程同步。
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
class IncrementThread implements Runnable {
private Counter counter;
public IncrementThread(Counter counter) {
this.counter = counter;
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
}
}
public class SynchronizedExample {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
IncrementThread incrementThread = new IncrementThread(counter);
Thread thread1 = new Thread(incrementThread);
Thread thread2 = new Thread(incrementThread);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("最终计数: " + counter.getCount());
}
}
线程通信示例
线程通信可以使用
wait()
、notify()
和notifyAll()
方法。
class Message {
private String msg;
private boolean empty = true;
public synchronized String read() {
while (empty) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
empty = true;
notifyAll();
return msg;
}
public synchronized void write(String msg) {
while (!empty) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
empty = false;
this.msg = msg;
notifyAll();
}
}
class Reader implements Runnable {
private Message message;
public Reader(Message message) {
this.message = message;
}
@Override
public void run() {
for (String latestMessage = message.read(); !"EOF".equals(latestMessage); latestMessage = message.read()) {
System.out.println("读取消息: " + latestMessage);
}
}
}
class Writer implements Runnable {
private Message message;
public Writer(Message message) {
this.message = message;
}
@Override
public void run() {
String[] messages = {"消息1", "消息2", "消息3", "EOF"};
for (String msg : messages) {
message.write(msg);
System.out.println("写入消息: " + msg);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
public class ThreadCommunicationExample {
public static void main(String[] args) {
Message message = new Message();
Thread readerThread = new Thread(new Reader(message));
Thread writerThread = new Thread(new Writer(message));
readerThread.start();
writerThread.start();
}
}
上述代码示例展示了创建线程的不同方式、线程同步以及线程通信的基本用法。通过继承
Thread
类和实现Runnable
接口创建线程,使用synchronized
关键字保证线程安全,利用wait()
、notify()
和notifyAll()
方法实现线程间的协作。
网络编程
Java 提供了丰富的网络编程类库,支持 TCP 和 UDP 协议。在 TCP 编程中,通过
Socket
类和ServerSocket
类实现客户端和服务器端的通信。例如,一个简单的 TCP 服务器端:
try (ServerSocket serverSocket = new ServerSocket(8080);
Socket socket = serverSocket.accept();
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) {
String inputLine;
while ((inputLine = in.readLine()) != null) {
System.out.println("收到客户端消息: " + inputLine);
out.println("消息已收到");
}
} catch (IOException e) {
e.printStackTrace();
}
客户端通过创建Socket
对象连接到服务器:
try (Socket socket = new Socket("localhost", 8080);
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
out.println("Hello, Server!");
String response = in.readLine();
System.out.println("收到服务器响应: " + response);
} catch (IOException e) {
e.printStackTrace();
}
UDP 编程则使用
DatagramSocket
类和DatagramPacket
类,适用于对实时性要求较高但对数据准确性要求相对较低的场景,如视频流、音频流传输。
Java 高级技术
反射机制
反射允许程序在运行时获取类的信息,包括类的属性、方法、构造函数等,并可以动态创建对象、调用方法。通过
Class
类及其相关方法实现反射。例如:
try {
Class<?> clazz = Class.forName("java.util.Date");
Object object = clazz.newInstance();
Method method = clazz.getMethod("getTime");
long time = (long) method.invoke(object);
System.out.println("当前时间毫秒数: " + time);
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
e.printStackTrace();
}
反射在框架开发、动态代理等场景中广泛应用,但由于反射操作绕过了编译器的检查,性能相对较低,应谨慎使用。
动态代理
动态代理是一种在运行时创建代理对象的机制,代理对象可以在不修改目标对象代码的情况下,对目标对象的方法调用进行增强。Java 的动态代理通过
Proxy
类和InvocationHandler
接口实现。例如:
interface HelloService {
void sayHello();
}
class HelloServiceImpl implements HelloService {
@Override
public void sayHello() {
System.out.println("Hello!");
}
}
class ProxyHandler implements InvocationHandler {
private Object target;
public ProxyHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("方法调用前增强");
Object result = method.invoke(target, args);
System.out.println("方法调用后增强");
return result;
}
}
public class Main {
public static void main(String[] args) {
HelloService target = new HelloServiceImpl();
HelloService proxy = (HelloService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new ProxyHandler(target));
proxy.sayHello();
}
}
动态代理在 AOP(面向切面编程)中起着核心作用,用于实现日志记录、事务管理等横切关注点。
注解
注解是 Java 5.0 引入的一种元数据机制,可以为程序元素(类、方法、字段等)添加额外的信息。注解分为预定义注解(如
@Override
、@Deprecated
、@SuppressWarnings
)和自定义注解。自定义注解需要使用@interface
关键字定义,并且可以通过反射读取注解信息。例如:
@interface MyAnnotation {
String value();
}
@MyAnnotation("This is a test")
class MyClass {
// 类体
}
public class Main {
public static void main(String[] args) {
MyClass myClass = new MyClass();
Class<?> clazz = myClass.getClass();
MyAnnotation annotation = clazz.getAnnotation(MyAnnotation.class);
if (annotation != null) {
System.out.println(annotation.value());
}
}
}
注解在框架开发中被大量使用,用于配置、代码生成等场景,提高了代码的可读性和可维护性。
通过对上述 Java 基础进阶知识的深入学习和实践,能够显著提升 Java 编程能力,为开发复杂、高效的 Java 应用程序奠定坚实基础。
=================Java基础知识到此完结=================
至此结束——
我是禹曦a
期待与你的下次相遇!!!