学习笔记
对Java泛型的理解
Java泛型是Java 5中引入的一个特性,允许在编码时提供类型安全,同时提供编译时类型检查,而不是运行时。它允许在定义类、接口和方法时使用类型参数。
类型擦除:Java泛型是通过类型擦除来实现的。这意味着在编译时,所有的泛型类型信息都会被移除,留下的代码只包含原始类型(Object类型)。这是因为在Java虚拟机(JVM)中,所有的类型信息在运行时都是不存在的。
边界限定:通过使用有界泛型,可以限制那些可以被传递到泛型类型参数的类型。比如,可以限定一个泛型只能接受实现了特定接口的类。
好处:
- 类型安全:避免了强制类型转换,提供编译时类型检查。
- 代码重用:同一份代码可以用于不同的数据类型。
限制:
- 类型擦除导致泛型类型参数在运行时不可用。
- 不能使用基本类型作为泛型类型参数(需要使用它们的包装类)。
泛型方法
泛型方法是在方法签名中使用类型参数的方法,这些类型参数在使用方法时被具体化。这意味着泛型方法可以与不同的类型一起工作。在Java中,泛型方法可以在静态和非静态上下文中使用。
例子:
public class Util {
// 这是一个泛型方法,使用类型参数T
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.print(element + " ");
}
System.out.println();
}
}
// 使用泛型方法
public static void main(String[] args) {
Integer[] intArray = {1, 2, 3, 4, 5};
String[] stringArray = {"a", "b", "c", "d"};
Util.printArray(intArray); // 输出: 1 2 3 4 5
Util.printArray(stringArray); // 输出: a b c d
}
泛型通配符
泛型通配符允许类型安全地创建可以与多种泛型类型一起工作的代码。它主要用于泛型集合,并且有两种类型:
- 上界通配符 (
? extends T):表示类型的上界,可以接受任何T类型或其子类型的集合。 - 下界通配符 (
? super T):表示类型的下界,可以接受任何T类型或其父类型的集合。
例子:
public void printList(List<? extends Number> list) {
for (Number number : list) {
System.out.println(number);
}
}
// 使用上界通配符,可以接受List<Number>或List<Integer>等
List<Integer> intList = Arrays.asList(1, 2, 3);
printList(intList);
在这个例子中,printList 方法可以接受任何继承自 Number 的类型的 List。这样的设计使得方法更加通用,可以接受多种类型的 List。
访问修饰符
- public:该成员对所有类可见。
- private:该成员仅在其所属的类中可见。
- protected:该成员在其所属的包中可见,并且对子类可见。
- default(没有修饰符):该成员在其所属的包中可见。
访问控制符与内类
访问控制符影响内部类的可见性:
- public:内部类可以被任何外部类访问。
- private:内部类只能被其外部类访问。
- protected:内部类可以被同一包下的类访问,也可以被其子类访问。
- default:内部类可以被同一包下的类访问。
静态绑定和动态绑定
静态绑定(也称为早期绑定)通常在编译时发生,它是基于变量的声明类型。静态绑定主要应用于静态方法、私有方法、构造函数和变量访问。
动态绑定(也称为晚期绑定)发生在运行时,基于对象的实际类型。动态绑定主要用于重写的方法。
public class Parent {
public void method() {
System.out.println("Parent method");
}
}
public class Child extends Parent {
@Override
public void method() {
System.out.println("Child method");
}
}
// 使用示例
Parent parent = new Child();
parent.method(); // 动态绑定,输出 "Child method"
在这个例子中,method()的调用将在运行时动态绑定到Child类的方法。
重写与重载
- 重写(Overriding)是在子类中重新定义父类中已经存在的方法,方法签名必须相同。
- 重载(Overloading)是在同一个类中定义多个同名方法,但它们的参数列表必须不同
控制结构
- if:如果条件为真,执行代码块。
- else:与if配对使用,如果条件为假,执行代码块。
- while:只要条件为真,就重复执行代码块。
- for:用于迭代数组或集合。
break:立即退出循环或switch语句。 continue:跳过当前循环的剩余部分,直接进行下一次迭代。
Java异常处理
Java异常处理是处理程序运行时可能出现的错误或异常情况的一种机制。
try-catch-finally:
- try:包含可能抛出异常的代码块。
- catch:捕获并处理异常。
- finally:无论是否发生异常,都会执行的代码块。
使用异常处理是为了使程序的错误处理更加健壮和可维护。
字符串池
字符串池是一个特殊的内存区域,用于存储字符串常量。当创建一个新的字符串对象时,如果字符串池中已经存在一个等价的字符串,那么会返回池中的字符串引用而不是创建一个新的对象。
静态变量和方法
静态变量和方法属于类而不是类的实例。它们可以通过类名直接访问,不需要创建类的实例。
内部类
- 成员内部类:作为外部类的一个成员。
- 局部内部类:在方法内定义并具有该方法范围的可见性。
- 匿名内部类:没有名称的局部内部类,通常用于重写方法或实现接口。
- 静态内部类:使用static定义,可以直接通过外部类名访问。
继承类型
- 单继承:Java不支持类的多继承,一个类只能有一个直接父类。
- 多继承:通过接口实现,一个类可以实现多个接口。
- 混合继承:不是Java的概念。
- 接口继承:接口可以继承另一个接口。
Java支持的继承类型是单继承和接口继承。
NPE(空指针异常)
NPE是Java中最常见的运行时异常之一,它发生在试图在一个空引用上执行操作时。在Java中,null表示没有对象,因此尝试访问null引用的成员或方法会导致NPE。
避免NPE的方法:
- 检查变量是否为null。
- 使用Java 8引入的Optional类来避免直接返回null。
- 使用断言(仅在开发和测试阶段)。
例子:
Object obj = // 获得对象引用
if (obj != null) {
// 安全地调用方法
obj.toString();
}
synchronized关键字
synchronized关键字用于控制对某个对象或方法的并发访问。它可以保证在同一时刻只有一个线程可以执行某个方法或代码块。(线程锁)
工作原理:
- 当一个线程访问对象的synchronized方法时,它会获得这个对象的锁。其他线程不能访问该对象的任何synchronized方法直到锁被释放。
- 对于静态synchronized方法,锁是关联于该类的Class对象。
多线程环境中的重要性:
- 避免多个线程同时修改同一资源,保证数据一致性。
- 控制对共享资源的访问,防止竞态条件。
例子:
public class SynchronizedExample {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized void decrement() {
count--;
}
}
Java基础知识
什么是JVM(Java虚拟机)?它是如何工作的?
JVM(Java虚拟机)是一个可以执行Java字节码的虚拟机进程。它是Java平台的核心组成部分,负责运行Java应用程序。JVM使得Java语言具有“一次编写,到处运行”的特性。
工作原理:
- 编译:Java源代码被编译器转换为字节码,这些字节码存储在.class文件中。
- 加载:JVM加载这些字节码文件到内存中。
- 验证:JVM验证加载的字节码,确保它们符合Java语言规范,没有安全问题。
- 执行:JVM通过解释器逐行执行字节码,或者通过即时编译器(JIT)将字节码转换为本地机器码执行,以提高性能。
- 垃圾回收:JVM负责管理内存,它会自动回收不再使用的对象占用的内存。
解释一下Java中的四种访问控制符及其作用范围。
Java提供了四种访问控制符,用于控制类、接口、方法和变量的访问级别:
public:公开的,可以在任何地方被访问。protected:受保护的,在同一包内可以被访问,或者在子类中也可以被访问。default(没有关键字,即不写任何访问控制符):默认的,在同一包内可以被访问。private:私有的,只能在定义它们的类内部被访问。
什么是静态绑定和动态绑定?请给出实例。
- 静态绑定(又称编译时绑定):在编译阶段,编译器根据对象的声明类型来决定调用哪个方法。静态绑定主要发生在方法重载和基本类型的操作中。
public class StaticBinding {
public void show(String s) {
System.out.println("String " + s);
}
public void show(Integer i) {
System.out.println("Integer " + i);
}
public static void main(String[] args) {
StaticBinding sb = new StaticBinding();
sb.show("Hello"); // 静态绑定,调用show(String)
sb.show(123); // 静态绑定,调用show(Integer)
}
}
- 动态绑定(又称运行时绑定):在程序运行时,根据对象的实际类型来决定调用哪个方法。动态绑定主要发生在多态和继承中。
class Parent {
public void show() {
System.out.println("Parent's show");
}
}
class Child extends Parent {
@Override
public void show() {
System.out.println("Child's show");
}
}
public class DynamicBinding {
public static void main(String[] args) {
Parent p = new Child(); // 多态
p.show(); // 动态绑定,调用Child的show方法
}
}
什么是泛型?为什么使用它们?简述类型擦除的概念。
泛型是Java 5引入的一种语言特性,它允许在编写代码时使用类型参数。这些类型参数在编译时被具体化,允许类和接口在设计时以一种类型安全的方式操作多种类型。
为什么使用它们:
- 类型安全:泛型允许编译时类型检查,避免了运行时类型转换错误。
- 代码复用:泛型减少了代码重复,提高了代码的可读性和可维护性。
类型擦除的概念: 类型擦除是指Java编译器在编译泛型代码时,移除了所有的类型参数信息,将泛型类型转换为其边界类型(如果没有指定边界,则转换为Object类型)。这意味着泛型信息不会存在于运行时,即JVM中不包含泛型信息。
例如:
List<String> list = new ArrayList<String>();
// 编译后,类型擦除发生,List<String>变成List
在上述代码中,List<String>在编译后会被擦除成List,因此在运行时,JVM只知道这是一个List,而不知道它原本是特定于String类型的。
Java高级特性
Java中的四种控制结构:
- 顺序结构:程序按照代码的书写顺序,从上到下依次执行。
- 分支结构:根据不同的条件执行不同的代码块,主要由
if-else和switch-case语句实现。 - 循环结构:重复执行某一段代码,直到条件不再满足为止,主要由
for、while和do-while循环实现。 - 跳转结构:用于无条件跳过某些代码或者循环的某一次迭代,主要由
break和continue语句实现。
Java异常处理机制的五大关键字:
try:尝试块,用来包围可能产生异常的代码。catch:捕获块,用来处理try块中抛出的异常。finally:最终块,无论是否捕获或处理异常,这里的代码都会被执行。throw:抛出异常,用于手动抛出一个异常。throws:声明异常,用于方法签名中,声明该方法可能会抛出的异常。
Java中的四种访问级别:
public:公开的,任何地方都可以访问。protected:保护的,同一个包内和子类可以访问。default(默认访问级别,无关键字):包私有,只有同一个包内的类可以访问。private:私有的,只有类本身内部可以访问。
内部类和匿名内部类的区别:
-
内部类(Inner Class):
- 是一个成员类,可以有一个或多个实例变量和方法。
- 可以被命名,并且可以继承一个类或实现接口。
- 可以有构造函数。
- 可以被其外部类之外的代码调用(如果它的访问权限允许)。
-
匿名内部类(Anonymous Inner Class):
- 没有名称,通常用于扩展一个类或实现接口的一次性使用。
- 没有构造函数,但是可以直接使用初始化代码块。
- 不能被重复使用,每次使用时都必须创建一个新的实例。
- 通常在方法的参数中或作为方法的返回值出现。
理解这些Java语言的基本概念对于编写结构清晰、健壮的Java程序是至关重要的。
Java集合框架
ArrayList和LinkedList的异同:
-
相同点:
- 都是Java Collection Framework的一部分,实现了
List接口。 - 都可以存储一系列有序的元素,可以根据索引位置访问和搜索元素。
- 都允许存储重复元素和
null值。
- 都是Java Collection Framework的一部分,实现了
-
不同点:
- 内部结构:ArrayList是基于动态数组的数据结构,而LinkedList是基于双向链表的数据结构。
- 性能:ArrayList在随机访问元素时(即通过索引访问)具有更好的性能,因为它是基于数组实现的;而LinkedList在添加或删除元素时具有更好的性能,因为链表的添加和删除操作不需要移动其他元素。
- 内存占用:LinkedList比ArrayList占用更多的内存,因为每个元素除了存储数据外,还需要存储两个引用,分别指向前一个和后一个元素。
- 扩容:ArrayList在容量不足时会自动扩容,而LinkedList不需要考虑容量问题。
HashMap的工作原理与哈希碰撞:
-
HashMap的工作原理:
- HashMap内部维护了一个数组,数组的每个元素是一个链表(或红黑树,当链表长度超过一定阈值时)。
- 当我们插入一个键值对时,首先使用键的
hashCode()方法计算出哈希值,然后根据哈希值计算出数组的索引位置,将键值对存储在对应位置的链表(或红黑树)中。 - 如果在查找时,发现两个不同的键具有相同的哈希值,会发生哈希碰撞。
- HashMap通过链表(或红黑树)解决哈希碰撞,即相同索引位置的所有元素形成一个链表(或红黑树),并在查找时遍历链表(或红黑树)以找到正确的元素。
-
哈希碰撞:
- 哈希碰撞是指两个不同的键经过哈希函数处理后得到了相同的哈希值。由于哈希表的容量是有限的,而可能的键是无限的,因此哈希碰撞是不可避免的。
- HashMap通过链表(或红黑树)来解决哈希碰撞,确保即使发生碰撞,也能正确存储和检索键值对。
实现一个线程安全的集合类:
- 要实现一个线程安全的集合类,可以使用Java提供的同步包装器类,如
Collections.synchronizedList()、Collections.synchronizedSet()等。 - 另一种方式是使用
Concurrent包下的并发集合类,如ConcurrentHashMap、CopyOnWriteArrayList等,这些集合内部已经实现了线程安全。 - 如果需要自定义线程安全的集合,可以在自定义类的方法中添加
synchronized关键字,确保同一时间只有一个线程可以访问该方法。 - 使用
ReentrantLock或Semaphore等并发控制工具也可以实现线程安全的集合类。
CopyOnWriteArrayList:
CopyOnWriteArrayList是一个线程安全的变体,其中所有可变操作(如添加、设置、删除等)都是通过对底层数组进行一次新的复制来实现的。- 在进行修改操作时,不直接在原始数组上进行修改,而是先复制出一个新的数组,然后在新数组上进行修改,修改完后再将原数组的引用指向新数组。
- 这种机制使得
CopyOnWriteArrayList在读操作时不需要加锁,因为内部数组永远不会改变;写操作时虽然需要复制数组,但读操作与写操作不会相互阻塞,适用于读多写少的场景。 CopyOnWriteArrayList的主要缺点是写操作的开销较大,因为每次写操作都需要复制整个底层数组。
Java多线程
线程与进程:
-
线程: 线程是操作系统能够进行运算调度的最小单位,被包含在进程之中,是进程中的实际运作单位。一个进程可以并发多个线程,每条线程并行执行不同的任务。线程共享进程资源,例如内存和文件句柄等,但每个线程有自己的栈空间和程序计数器。
-
进程: 进程是计算机中程序关于某数据集合的一次运行活动,是系统进行资源分配和调度的基本单位。每个进程都有独立的地址空间、内存、数据栈以及其他用于跟踪执行的辅助数据。进程可以通过创建线程来执行并行任务。
Java中实现多线程的两种方法:
-
继承
Thread类: 在Java中,可以通过继承Thread类并重写run()方法来创建一个线程。创建完自定义的线程类之后,可以实例化这个类,并调用start()方法来启动线程。class MyThread extends Thread { public void run() { // 线程执行的代码 } } MyThread t = new MyThread(); t.start(); -
实现
Runnable接口: 另一种方法是实现Runnable接口,并实现run()方法。然后将该Runnable对象传递给Thread类的构造器,并通过Thread对象调用start()方法。class MyRunnable implements Runnable { public void run() { // 线程执行的代码 } } MyRunnable r = new MyRunnable(); Thread t = new Thread(r); t.start();
同步与 synchronized 关键字:
-
同步: 同步是指在多线程环境中,对共享资源的操作进行合理的控制,以避免多个线程同时修改同一资源而引发的数据不一致问题。
-
synchronized关键字:synchronized是Java中的一个关键字,用于实现同步机制。它可以修饰方法或代码块,确保同一时间只有一个线程可以执行该段代码。synchronized关键字锁定的是一个对象,对于同步方法,锁定的是调用该方法的对象;对于同步代码块,锁定的是括号内的对象。// 同步方法 public synchronized void synchronizedMethod() { // ... } // 同步代码块 public void synchronizedBlock() { synchronized(this) { // ... } }
死锁与避免死锁:
-
死锁: 死锁是指多个线程在等待对方持有的资源而无限期阻塞的现象。在死锁的情况下,涉及的线程都无法继续执行。
-
避免死锁:
-
资源有序分配:确保所有线程按照相同的顺序请求资源,可以防止线程因资源互相等待而造成死锁。
-
超时尝试:为资源请求设置超时时间,如果线程在指定时间内没有获取到资源,则会放弃,回退并重新尝试,这样可以打破死锁。
-
资源预分配:在执行之前,预先分配所有必要的资源给线程,确保线程在执行过程中不需要等待其他资源。
-
避免循环等待:设计程序时,避免线程之间形成循环等待关系。
-
使用并发库中的高级同步工具:如
java.util.concurrent包提供的锁和同步工具,它们提供了避免死锁的机制。-
java.util.concurrent是 Java 平台提供的一个包含并发编程实用工具的包。这个包为Java并发编程提供了强大的支持,包括线程池、异步计算、同步工具、并发集合等。使用这些工具可以帮助开发者更容易地编写出高效且安全的并发程序。以下是一些
java.util.concurrent包中常用的类和接口:- ExecutorService:
- 这是一个接口,用于执行提交的
Runnable任务。它提供了一种将任务提交与每个任务的执行机制分离的方法。线程池是实现此接口的一个例子。
- 这是一个接口,用于执行提交的
- ThreadPoolExecutor:
- 这是一个实现了
ExecutorService的线程池,它允许你控制线程的数量以及它们如何被管理。
- 这是一个实现了
- Future:
- 这是一个接口,表示异步计算的结果。你可以使用
Future来获取计算结果,取消任务,或者检查任务是否已经完成。
- 这是一个接口,表示异步计算的结果。你可以使用
- Callable:
- 这是一个接口,与
Runnable类似,但它允许返回一个结果或抛出一个异常。
- 这是一个接口,与
- ForkJoinPool:
- 这是一个工作窃取算法的线程池,适用于递归任务和分治算法。
- CountDownLatch:
- 这是一个同步辅助类,允许一个或多个线程等待直到一系列操作在其他线程中执行完成。
- CyclicBarrier:
- 这是一个同步辅助类,允许一组线程互相等待,直到所有线程都达到某个屏障点。
- Semaphore:
- 这是一个计数信号量,主要用于限制可以同时访问某个特定资源的线程数量。
- ReentrantLock:
- 这是一个可重入的互斥锁,比
synchronized方法和块更灵活。
- 这是一个可重入的互斥锁,比
- ConcurrentHashMap:
- 这是一个线程安全的哈希表,它允许完全并发的读取和一定程度的并发写入。
- ConcurrentLinkedQueue、ConcurrentLinkedDeque:
- 这些是线程安全的队列和双端队列。
- BlockingQueue:
- 这是一个接口,用于支持阻塞队列的操作。线程池经常使用阻塞队列来存储等待执行的任务。
使用
java.util.concurrent包中的类和接口可以帮助开发者处理并发编程中的许多复杂问题,如线程创建和管理、同步、资源竞争、死锁等。这些工具也是现代Java并发编程中不可或缺的一部分。 - ExecutorService:
-
-
Java网络编程
Socket编程:
Socket编程是一种用于实现网络通信的技术。它允许不同计算机上的程序通过网络进行数据交换。Socket编程通常基于传输控制协议(TCP)或用户数据报协议(UDP)。
工作原理:
-
客户端-服务器模型:Socket编程通常基于客户端-服务器模型。客户端创建一个Socket连接到服务器的IP地址和端口号,然后通过该连接发送或接收数据。
-
TCP连接:对于基于TCP的Socket编程,连接的建立需要经过“三次握手”过程,确保数据传输的可靠性。客户端发送请求,服务器接受连接,然后客户端确认,完成握手。
-
数据传输:一旦连接建立,客户端和服务器就可以通过Socket进行数据传输。数据被封装成数据包,通过网络传输。
-
关闭连接:通信完成后,一方会关闭连接,然后另一方也会关闭,完成“四次挥手”过程。
-
三次握手和四次挥手是TCP(传输控制协议)中用于建立和终止连接的两个重要过程。
三次握手(建立连接)
三次握手是TCP协议中用于建立一个连接的过程。它的目的是确保客户端和服务器双方都具备发送和接收数据的能力,并同步双方的初始序列号。以下是三次握手的步骤:
-
SYN:
- 客户端发送一个SYN(同步序列编号)报文到服务器,并进入SYN_SENT状态,等待服务器确认。
- 这个报文中包含一个客户端的初始序列号。
-
SYN-ACK:
- 服务器接收到客户端的SYN报文后,会发送一个SYN-ACK(同步确认应答)报文作为应答。
- 这个报文中包含服务器的初始序列号,同时也对客户端的SYN进行确认。
- 服务器此时进入SYN_RCVD状态。
-
ACK:
- 客户端收到服务器的SYN-ACK报文后,会发送一个ACK(确认)报文作为应答。
- 这个ACK报文中的确认号是服务器的初始序列号加1。
- 客户端此时进入ESTABLISHED状态,服务器在收到这个ACK报文后也进入ESTABLISHED状态。
- 连接建立成功,双方可以开始数据传输。
四次挥手(终止连接)
四次挥手是TCP协议中用于终止一个连接的过程。由于TCP连接是全双工的,因此每个方向必须单独进行关闭。以下是四次挥手的步骤:
-
FIN:
- 当通信结束时,发起关闭的一方(可以是客户端或服务器)会发送一个FIN(结束)报文到另一方,并进入FIN_WAIT_1状态,等待对方确认。
-
ACK:
- 收到FIN报文的一方会发送一个ACK报文作为应答,确认序号为收到序号加1。
- 发送方此时进入FIN_WAIT_2状态,等待对方发送FIN报文。
-
FIN:
- 在发送完ACK报文并处理完剩余的数据传输后,收到FIN的一方也发送一个FIN报文,并进入LAST_ACK状态。
-
ACK:
- 收到对方FIN报文的一方发送一个ACK报文作为应答,然后进入TIME_WAIT状态。
- 经过一段时间(称为2MSL,即最大报文生存时间的两倍)后,确保对方收到了最后的ACK报文,然后关闭连接。
这样,TCP连接就被成功终止了。四次挥手过程比三次握手复杂,因为它需要确保双方都没有数据需要传输后才能完全关闭连接。
-
Java中的BIO、NIO和AIO:
- BIO (Blocking I/O):传统的Java I/O操作是阻塞式的。当进行读/写操作时,线程会等待直到操作完成。这种模式在处理大量并发连接时效率低下,因为每个连接都需要一个线程来处理。
- NIO (New I/O):Java NIO提供了非阻塞I/O操作。它使用选择器(Selector)来检查一个或多个通道(Channel)是否准备好进行读或写操作。这使得单个线程可以管理多个通道,从而提高资源利用率。
- AIO (Asynchronous I/O):异步I/O是Java 7引入的。在AIO中,线程提交请求后可以立即返回,而不是等待操作完成。当操作完成后,线程会被通知,然后处理结果。
HTTP协议:
HTTP(超文本传输协议)是用于从服务器传输超文本到本地浏览器的应用层协议。它定义了客户端和服务器之间请求和响应的格式。
使用Java实现HTTP请求: 在Java中,可以通过以下几种方式实现HTTP请求:
- 使用
java.net.HttpURLConnection类,这是一个内建的Java类,可以用来发送HTTP请求。 - 使用
org.apache.httpcomponents库中的HttpClient,这是一个更加强大的工具,支持各种HTTP方法、请求头、连接池等。 - 使用第三方库如 OkHttp 或 Retrofit。
以下是使用
HttpURLConnection发送GET请求的简单示例:
URL url = new URL("http://www.example.com");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept", "application/json");
int responseCode = conn.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String inputLine;
StringBuilder response = new StringBuilder();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
System.out.println(response.toString());
}
conn.disconnect();
上述代码创建了一个URL连接,发送了GET请求,并读取了响应内容。
Java新特性
Java 8中的Lambda表达式
Lambda表达式是Java 8引入的一个新特性,它提供了一种简洁的语法来表示一个方法的实现,可以作为匿名函数使用。Lambda表达式允许你以更紧凑、更灵活的方式表达实例的一些单方法接口(functional interfaces)。
Lambda表达式的基本语法如下:
(parameters) -> expression
或
(parameters) -> { statements; }
例如,以下是一个使用Lambda表达式的例子,它实现了Runnable接口:
Runnable run = () -> System.out.println("Hello, Lambda!");
new Thread(run).start();
在这个例子中,() -> System.out.println("Hello, Lambda!");就是一个Lambda表达式,它实现了Runnable接口的run方法。
Java Stream API
Java Stream API是Java 8中引入的另一个新特性,用于处理序列化的数据(比如集合、数组等)。它支持顺序和并行处理,可以很容易地执行复杂的操作,如过滤、映射、限制、减少等。
下面是一个使用Java Stream API的例子,用于过滤和打印集合中的偶数:
import java.util.Arrays;
import java.util.List;
public class StreamExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
numbers.stream()
.filter(n -> n % 2 == 0) // 过滤出偶数
.forEach(System.out::println); // 打印偶数
}
}
在这个例子中,我们创建了一个流,然后使用filter方法过滤出集合中的偶数,最后使用forEach方法打印出来。
Java 9引入的模块化系统
Java 9引入了一个全新的模块化系统,这个系统旨在帮助开发者更好地组织和管理大型项目。模块是一个包含相关类和资源的集合,它定义了哪些部分对外可见,哪些是私有的。
模块化系统的关键特点包括:
- 模块声明:使用
module-info.java文件来声明模块的名称、依赖关系和导出的包。 - 强封装:默认情况下,模块内部的包对其他模块是不可见的,除非明确导出。
- 依赖管理:模块可以声明它们需要哪些其他模块,这使得依赖关系更加明确和易于管理。
- 服务:模块可以使用服务来发现实现特定接口的其他模块。
下面是一个简单的模块声明示例:
module com.example.myapp {
// 模块名称为 com.example.myapp
requires java.base; // 声明此模块需要的基础模块
exports com.example.package1; // 导出指定的包
}
这个模块声明指明了模块的名称、它依赖的Java基础模块,以及它导出的包。这样,其他模块可以明确地知道它们可以访问哪些类和接口。
设计模式
1. 单例模式的工作原理及其在Java中的实现
单例模式(Singleton Pattern)是一种创建型设计模式,用于确保一个类只有一个实例,并提供一个全局访问点来获取该实例。这种模式确保了在应用程序中共享资源时,不会因为创建了多个实例而导致状态不一致的问题。
工作原理:
- 私有构造器:确保外部无法通过
new关键字创建类的实例。 - 静态变量:保存类的唯一实例。
- 公有静态方法:提供全局访问点,用于获取该类的实例。
Java中的实现:
以下是单例模式的一种常见实现,使用懒汉式加载(懒加载):
public class Singleton {
// 私有静态变量,用于存储单例的引用
private static Singleton instance;
// 私有构造器,防止外部实例化
private Singleton() {}
// 公有静态方法,提供全局访问点
public static Singleton getInstance() {
// 如果实例还没有被创建,则创建一个实例
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
注意:上述实现没有考虑线程安全,在多线程环境下可能存在问题。为了实现线程安全,可以使用同步锁或者使用饿汉式加载。
2. 工厂模式
工厂模式(Factory Pattern)是一种创建型设计模式,用于创建对象而不必暴露创建逻辑给客户端,通过使用一个共同的接口来指向新创建的对象。
解决的问题:
- 当创建对象的逻辑较为复杂或者需要根据不同的情况创建不同的对象时,工厂模式可以封装这种创建逻辑,提高代码的可维护性和可扩展性。
- 避免客户端直接依赖于具体类,减少系统间的耦合关系,使得系统更加模块化。
工厂模式主要有以下几种变体:
- 简单工厂模式:一个工厂类根据传入的参数决定创建哪一种产品类。
- 工厂方法模式:定义一个接口用于创建对象,但让子类决定实例化哪一个类,工厂方法使一个类的实例化延迟到其子类。
- 抽象工厂模式:提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类。
3. 观察者模式
观察者模式(Observer Pattern)是一种行为型设计模式,它定义了一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知并自动更新。
主要组成:
- 观察者(Observer):定义了一个更新接口,用于当主题对象状态改变时被通知。
- 主题(Subject):维护一组观察者,并提供添加、删除和通知观察者的方法。
- 具体主题(ConcreteSubject):实现主题接口,当自身状态改变时,通知所有注册的观察者。
- 具体观察者(ConcreteObserver):实现观察者接口,以便在主题状态改变时更新自己。
优点:
- 支持简单的广播通信,自动通知所有注册过的对象。
- 当主题和观察者之间的依赖关系改变时,易于增加和删除观察者。
- 遵循开闭原则,增加新的具体观察者无需修改主题的代码。
观察者模式的典型应用场景包括事件处理、用户界面更新、模型-视图控制器模式等。