What is Java ?(三)

内容:注解异常ObjectJava8新特性

本篇博客是笔者作为初学者记录自己对Java一些基本概念的理解。内容参考了大量网络资源,篇幅很长,旨在作为个人学习笔记,供自己日后回顾和复习。

注解

在Java中,注解(Annotation)是一种元数据(metadata),它提供了关于程序代码的额外信息,但本身并不直接影响程序的执行。注解可以用于类、方法、字段、参数、局部变量等程序元素上,用于在编译时、运行时或部署时提供额外的信息。

注解本质是一个继承了Annotation的特殊接口,其具体实现类是Java运行时生成的动态代理类。我们通过反射获取注解时,返回的是Java运行时生成的动态代理对象。通过代理对象调用自定义注解的方法,会最终调用AnnotationlnvocationHandler的invoke方法。该方法会从memberValues这个Map中索引出对应的值。而memberValues的来源是Java常量池。

注解的原理

  1. 定义注解
    注解是通过@interface关键字定义的。例如:

    1
    2
    3
    4
    public @interface MyAnnotation {
    String value();
    int count() default 1;
    }

    这个注解MyAnnotation有两个元素:valuecountvalue是必需的,而count有一个默认值1

  2. 元注解
    元注解是用于注解其他注解的注解。Java提供了几个内置的元注解,用于控制注解的行为:

    • @Retention:指定注解的保留策略,即注解在什么阶段有效(源码、编译时、运行时)。

      • RetentionPolicy.SOURCE:注解仅在源码中保留,编译时丢弃。
      • RetentionPolicy.CLASS:注解在编译时保留,但运行时不可见。
      • RetentionPolicy.RUNTIME:注解在运行时保留,可以通过反射获取。
    • @Target:指定注解可以应用的目标类型(类、方法、字段等)。

      • ElementType.TYPE:类、接口、枚举。
      • ElementType.METHOD:方法。
      • ElementType.FIELD:字段。
      • ElementType.PARAMETER:方法参数。
      • ElementType.CONSTRUCTOR:构造函数。
      • ElementType.LOCAL_VARIABLE:局部变量。
      • ElementType.ANNOTATION_TYPE:注解类型。
      • ElementType.PACKAGE:包。
    • @Documented:指定注解是否包含在JavaDoc中。

    • @Inherited:指定注解是否可以被子类继承。

    • @Repeatable:指定注解是否可以重复应用在同一个元素上。

  3. 使用注解
    注解可以应用在类、方法、字段等程序元素上。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @MyAnnotation(value = "Hello", count = 5)
    public class MyClass {
    @MyAnnotation(value = "Field")
    private String myField;

    @MyAnnotation(value = "Method")
    public void myMethod() {
    // ...
    }
    }
  4. 处理注解
    注解本身并不做任何事情,它们需要通过某种方式被处理才能发挥作用。处理注解的方式主要有两种:

    • 编译时处理:使用注解处理器(Annotation Processor)在编译时处理注解。例如,Lombok库使用注解处理器在编译时生成代码。

    • 运行时处理:使用反射(Reflection)在运行时获取注解信息。例如:

      1
      2
      3
      4
      5
      6
      7
      public void processAnnotation(Class<?> clazz) {
      if (clazz.isAnnotationPresent(MyAnnotation.class)) {
      MyAnnotation annotation = clazz.getAnnotation(MyAnnotation.class);
      System.out.println("Value: " + annotation.value());
      System.out.println("Count: " + annotation.count());
      }
      }

      这个方法通过反射检查类上是否存在MyAnnotation注解,并获取注解的值。

应用作用域

在Java中,注解可以应用于多种程序元素,包括类、方法、字段(属性)、参数、局部变量等。注解的作用域(即注解可以应用的目标类型)由@Target元注解指定。以下是一些常见的注解作用域及其对应的ElementType枚举值:

1. 类

注解可以应用于类、接口、枚举等类型定义上。对应的ElementTypeTYPE

1
2
3
4
5
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ClassAnnotation {
String value();
}

使用示例:

1
2
3
4
@ClassAnnotation("This is a class annotation")
public class MyClass {
// ...
}

2. 方法

注解可以应用于方法定义上。对应的ElementTypeMETHOD

1
2
3
4
5
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodAnnotation {
String value();
}

使用示例:

1
2
3
4
5
6
public class MyClass {
@MethodAnnotation("This is a method annotation")
public void myMethod() {
// ...
}
}

3. 属性(字段)

注解可以应用于字段(属性)定义上。对应的ElementTypeFIELD

1
2
3
4
5
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FieldAnnotation {
String value();
}

使用示例:

1
2
3
4
5
6
public class MyClass {
@FieldAnnotation("This is a field annotation")
private String myField;

// ...
}

其他作用域

除了上述常见的类、方法和字段,注解还可以应用于其他程序元素:

  • 参数:注解可以应用于方法参数上。对应的ElementTypePARAMETER

    1
    2
    3
    4
    5
    @Target(ElementType.PARAMETER)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface ParameterAnnotation {
    String value();
    }

    使用示例:

    1
    2
    3
    4
    5
    public class MyClass {
    public void myMethod(@ParameterAnnotation("This is a parameter annotation") String param) {
    // ...
    }
    }
  • 构造函数:注解可以应用于构造函数上。对应的ElementTypeCONSTRUCTOR

    1
    2
    3
    4
    5
    @Target(ElementType.CONSTRUCTOR)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface ConstructorAnnotation {
    String value();
    }

    使用示例:

    1
    2
    3
    4
    5
    6
    public class MyClass {
    @ConstructorAnnotation("This is a constructor annotation")
    public MyClass() {
    // ...
    }
    }
  • 局部变量:注解可以应用于局部变量上。对应的ElementTypeLOCAL_VARIABLE

    1
    2
    3
    4
    5
    @Target(ElementType.LOCAL_VARIABLE)
    @Retention(RetentionPolicy.SOURCE)
    public @interface LocalVariableAnnotation {
    String value();
    }

    使用示例:

    1
    2
    3
    4
    5
    6
    7
    public class MyClass {
    public void myMethod() {
    @LocalVariableAnnotation("This is a local variable annotation")
    String localVar = "Hello";
    // ...
    }
    }
  • 注解类型:注解可以应用于其他注解类型上。对应的ElementTypeANNOTATION_TYPE

    1
    2
    3
    4
    5
    @Target(ElementType.ANNOTATION_TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface MetaAnnotation {
    String value();
    }

    使用示例:

    1
    2
    3
    4
    5
    6
    @MetaAnnotation("This is a meta-annotation")
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface MyMethodAnnotation {
    String value();
    }
  • :注解可以应用于包声明上。对应的ElementTypePACKAGE

    1
    2
    3
    4
    5
    @Target(ElementType.PACKAGE)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface PackageAnnotation {
    String value();
    }

    使用示例:

    1
    2
    3
    // 在 package-info.java 文件中
    @PackageAnnotation("This is a package annotation")
    package com.example;

异常

啥是异常?

Java异常主要基于两大类:Throwable类及其子类。Throwable的子类主要有两个重要的子类:ErrorException,它们分别代表了不同类型的异常情况。

Error

Error表示运行环境错误,极难恢复,不能指望程序自己处理这些异常。比如系统崩溃、连接错误等。比如OutOfMemoryError内存不足错误、StackOverflowError栈溢出错误。通常程序不应该去捕获这些错误,因为它们表示系统级别的严重问题,程序无法自行恢复。

Exception

Exception表示运行程序错误,根据发生时期又可以分为编译时异常运行时异常

编译时异常(Checked Exception)

在代码编译时出现的异常,必须显式处理(捕获或声明抛出)。这些异常通常是程序外部的异常,比如文件不存在、无法找到类等。对于这些异常必须使用try-catch块捕获异常,或者在方法签名中使用throws关键字声明抛出异常。

运行时异常(Unchecked Exception)

程序运行过程中出现的异常,通常由程序逻辑错误引起,不需要显式处理。

  • 常见异常
    • NullPointerException:空指针异常。
    • IllegalArgumentException:非法参数异常。
    • ClassCastException:类转换异常。
    • IndexOutOfBoundsException:数组越界异常。

虽然运行时异常不需要显式处理,但建议在代码中进行适当的检查和处理,以提高程序的健壮性。

异常处理机制

Java提供了异常处理机制,通过try-catch语句块来捕获和处理异常。以下是Java常见的异常处理方式:

  • try块:包含可能抛出异常的代码。

  • catch块:捕获并处理异常。

  • throw语句:用于手动抛出异常。

  • finally块:无论是否发生异常,都会执行的代码块,通常用于资源清理。

示例代码

以下是一个完整的异常处理示例,展示了如何使用try-catch语句块、throw语句和finally块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class ExceptionHandlingExample {
public static void main(String[] args) {
try {
// 调用可能抛出异常的方法
readFile("nonexistentfile.txt");
} catch (FileNotFoundException e) {
// 捕获并处理FileNotFoundException
System.err.println("File not found: " + e.getMessage());
} catch (IOException e) {
// 捕获并处理IOException
System.err.println("IO error: " + e.getMessage());
} finally {
// 无论是否发生异常,都会执行的代码块
System.out.println("Finally block executed.");
}
}

public static void readFile(String fileName) throws FileNotFoundException, IOException {
// 使用throws语句声明可能抛出的异常
FileInputStream fileInputStream = null;
try {
fileInputStream = new FileInputStream(fileName);
// 读取文件内容
int data = fileInputStream.read();
while (data != -1) {
System.out.print((char) data);
data = fileInputStream.read();
}
} catch (FileNotFoundException e) {
// 手动抛出FileNotFoundException
throw new FileNotFoundException("Custom message: " + e.getMessage());
} finally {
// 确保文件流被关闭
if (fileInputStream != null) {
try {
fileInputStream.close();
} catch (IOException e) {
System.err.println("Error closing file: " + e.getMessage());
}
}
}
}
}

抛出异常为什么不用throws?

如果说异常是未检查异常(Unchecked Exception)或已经在方法内部处理,就不需要再使用throws声明了。

  • Unchecked Exception:未检查异常,是继承自RuntimeException类或者Error类的异常,编译器不强制要求进行异常处理。因此对于这些异常,不需要在方法签名中使用throws来声明。

  • 捕获和异常处理:另一种常见情况是已经在方法内部捕获了可能抛出的异常并在方法内部处理它们。

try-catch的执行顺序

正常情况下会按顺序执行try块中的代码,当运行过程中出现异常则跳转到相应的catch异常捕获块,最后无论是否出现异常,总会执行finally块。

常见问题

以下这段代码最后会返回什么?

1
2
3
4
5
try {
return 'a';
} finally {
return 'b';
}

答案是返回'b'。因为tryreturn语句先执行,压入返回栈中,而finally中的return方法后执行,也压入栈中,返回结果先弹出栈上方的元素,所以会返回'b',也就是说finally中的return会覆盖try块的返回语句。

示例代码

以下是一个示例,展示了try-catch-finally块的执行顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class TryCatchFinallyExample {
public static void main(String[] args) {
System.out.println("Return value: " + getValue());
}

public static char getValue() {
try {
System.out.println("Executing try block");
return 'a';
} finally {
System.out.println("Executing finally block");
return 'b';
}
}
}

输出结果:

1
2
3
Executing try block
Executing finally block
Return value: b

在这个示例中,try块中的return 'a'语句先执行,但被finally块中的return 'b'语句覆盖,因此最终返回值为'b'

Object

“==”与“equals”有什么区别?

对于字符串变量,==equals方法是不同的。==判断的是两个对象的引用是否相同,即比较两个对象的内存首地址是否指向同一个对象,而equals则判断的是两个对象的值是否相同。

示例代码片段,对于字符串来说:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class StringComparisonExample {
public static void main(String[] args) {
String str1 = "hello";
String str2 = "hello";
String str3 = new String("hello");

// 使用 == 比较
System.out.println(str1 == str2); // true,因为str1和str2指向同一个字符串常量池中的对象
System.out.println(str1 == str3); // false,因为str3是新创建的对象,内存地址不同

// 使用 equals 比较
System.out.println(str1.equals(str2)); // true,值相同
System.out.println(str1.equals(str3)); // true,值相同
}
}

对于非字符串来说,若没有对equals方法重写,则==equals是相同的,都是比较两个对象的内存首地址是否相同,即比较两个对象的引用是否指向同一个变量。

示例代码片段,对于非字符串对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ObjectComparisonExample {
public static void main(String[] args) {
Object obj1 = new Object();
Object obj2 = new Object();
Object obj3 = obj1;

// 使用 == 比较
System.out.println(obj1 == obj2); // false,内存地址不同
System.out.println(obj1 == obj3); // true,内存地址相同

// 使用 equals 比较
System.out.println(obj1.equals(obj2)); // false,默认比较内存地址
System.out.println(obj1.equals(obj3)); // true,默认比较内存地址
}
}

“equals”与“hashCode”

equals方法用于比较两个对象是否相等,而hashCode方法返回对象的哈希码值。在Java中,如果两个对象通过equals方法比较相等,那么它们的hashCode值必须相同。反之,如果两个对象的hashCode值相同,它们不一定相等。

示例代码片段

1
2
3
4
5
6
7
8
9
10
11
12
13
public class EqualsHashCodeExample {
public static void main(String[] args) {
String str1 = "hello";
String str2 = "hello";

// 比较 equals
System.out.println(str1.equals(str2)); // true

// 比较 hashCode
System.out.println(str1.hashCode()); // 输出哈希码值
System.out.println(str2.hashCode()); // 输出相同的哈希码值
}
}

StringBuilder和StringBuffer

由于String类型是不可变的(immutable),每次对String进行修改操作时,都会创建一个新的String对象,这可能会导致性能问题。为了解决这个问题,Java提供了StringBuilderStringBuffer类,它们是可变的(mutable),允许在原有对象上进行修改操作。

区别

  • 线程安全

    • StringBuffer是线程安全的,适用于多线程环境。
    • StringBuilder不是线程安全的,适用于单线程环境,性能更高。
  • 性能

    • 通常情况下,StringBuilder的性能优于StringBuffer,因为StringBuffer需要维护线程安全,会带来额外的开销。
    • String的性能通常是最差的,因为每次修改都会创建新的对象。
  • 使用场景

    • 在单线程环境中,推荐使用StringBuilder以获得最佳性能。
    • 在多线程环境中,如果需要线程安全,使用StringBuffer

示例代码片段

1
2
3
4
5
6
7
8
9
10
11
12
13
public class StringBuilderStringBufferExample {
public static void main(String[] args) {
// 使用 StringBuilder
StringBuilder sb = new StringBuilder("Hello");
sb.append(" World");
System.out.println(sb.toString()); // 输出 "Hello World"

// 使用 StringBuffer
StringBuffer sbuf = new StringBuffer("Hello");
sbuf.append(" World");
System.out.println(sbuf.toString()); // 输出 "Hello World"
}
}

Java 1.8 新特性

Stream API

Java 8 引入了 Stream API,提供了一种更加高效的数据处理方式,特别是对于集合的操作如过滤、排序、映射等。使用 Stream 能够充分利用多核处理器的优势进行并行处理。

示例

以下是一个简单的示例,展示了如何使用 Stream API 对集合进行操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class StreamExample {
public static void main(String[] args) {
// 创建一个包含整数的列表
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// 使用 Stream API 过滤偶数并计算平方
List<Integer> evenSquares = numbers.stream()
.filter(n -> n % 2 == 0) // 过滤偶数
.map(n -> n * n) // 计算平方
.collect(Collectors.toList()); // 收集结果到列表

// 输出结果
System.out.println("Even squares: " + evenSquares);

// 使用并行流进行相同的操作
List<Integer> evenSquaresParallel = numbers.parallelStream()
.filter(n -> n % 2 == 0) // 过滤偶数
.map(n -> n * n) // 计算平方
.collect(Collectors.toList()); // 收集结果到列表

// 输出结果
System.out.println("Even squares (parallel): " + evenSquaresParallel);
}
}

解释

  1. 创建列表

    • 使用 Arrays.asList 创建一个包含整数的列表。
  2. 使用 Stream API

    • numbers.stream():创建一个顺序流。
    • filter(n -> n % 2 == 0):过滤出偶数。
    • map(n -> n * n):将每个偶数计算平方。
    • collect(Collectors.toList()):将结果收集到一个列表中。
  3. 使用并行流

    • numbers.parallelStream():创建一个并行流,充分利用多核处理器的优势。

输出

1
2
Even squares: [4, 16, 36, 64, 100]
Even squares (parallel): [4, 16, 36, 64, 100]

Stream流的并行API

ParallelStream。并行流其实带着“分而治之”的思想,在处理源数据时,将数据分成多个子流,然后将处理结果汇总为一个流对象。底层逻辑是使用 Fork/Join 框架来实现并行处理。

对CPU密集型的任务来说,并行流使用ForkJoinPool线程池,为每个CPU分配一个任务,这是非常有效率的,但是如果任务不是CPU密集的,而是I/O密集的,并且任务数相对线程数比较大,那么直接用Parallelstream并不是很好的选择。

CompletableFuture

CompletableFuture 是 Java 8 引入的一个类,用于支持异步编程和非阻塞操作。它实现了 Future 接口,并提供了更强大的功能,如组合多个异步任务、处理异常、以及在任务完成时执行回调等。

主要功能

  1. 异步执行任务:使用 CompletableFuture.supplyAsyncCompletableFuture.runAsync 方法来异步执行任务。

  2. 任务组合:使用 thenApplythenAcceptthenRun 等方法来组合多个异步任务。

  3. 异常处理:使用 exceptionally 方法来处理异常。

  4. 任务完成时的回调:使用 whenComplete 方法在任务完成时执行回调。

示例

以下是一个简单的示例,展示了如何使用 CompletableFuture 进行异步编程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class CompletableFutureExample {
public static void main(String[] args) {
// 异步执行任务
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(1000); // 模拟耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Hello, CompletableFuture!";
});

// 任务完成时的回调
future.thenAccept(result -> {
System.out.println("Result: " + result);
});

// 异常处理
future.exceptionally(ex -> {
System.err.println("Exception: " + ex.getMessage());
return null;
});

// 等待任务完成
try {
future.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}

解释

  1. 异步执行任务CompletableFuture.supplyAsync(() -> { ... }):异步执行一个返回值为 String 的任务。

  2. 任务完成时的回调future.thenAccept(result -> { ... }):在任务完成时执行回调,输出结果。

  3. 异常处理future.exceptionally(ex -> { ... }):处理任务执行过程中可能抛出的异常。

  4. 等待任务完成future.get():等待任务完成并获取结果。

输出

1
Result: Hello, CompletableFuture!

再来一个简单的组合示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Example {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(5);

CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> {
System.out.println("执行step 1");
return "step1 result";
}, executor);

CompletableFuture<String> cf2 = CompletableFuture.supplyAsync(() -> {
System.out.println("执行step 2");
return "step2 result";
});

cf1.thenCombine(cf2, (result1, result2) -> {
System.out.println(result1 + " + " + result2);
System.out.println("执行step 3");
return "step3 result";
}).thenAccept(result3 -> System.out.println(result3));

// 关闭线程池
executor.shutdown();
}
}