本篇将涉及 Java 中的基础知识,主要是别的语言中没有或者很少有的特性。
反射
Java 中,除了基本数据类型外,其他所有类型都是 class 类型。当加载一个类时,JVM 会为这个类创建一个 Class 类型的对象,这个对象包含了这个类的所有信息。这个对象就是反射的基础。
要获取一个类的 Class 对象,有三种方法。例如,我们可以这样获取 String 类的 Class 对象:
-
通过类本身:
Class<?> clazz = String.class; -
通过完整的类名:
Class<?> clazz = Class.forName("java.lang.String"); -
通过类的实例:
String str = "A Random String Instance"; Class<?> clazz = str.getClass();
类在 JVM 中的 Class 是唯一的,因此这三种方法获取的 Class 对象是同一个,可以直接用 == 比较。
获取到 Class 对象后,我们就可以得到它的一些基本信息。你可以在 Java doc 中找到。经常用到的包括:
类名和包名
可以通过 getName() 方法获取完整的类名,通过 getSimpleName() 方法获取类名。
clazz.getName(); // java.lang.String
clazz.getSimpleName(); // String
可以通过 getPackage() 方法获取包名。
clazz.getPackage().getName(); // java.lang
修饰符
返回结果是一个整数,具体数值对应的修饰符可以参考 JVM 文档 Table 4.1-B。
clazz.getModifiers(); // 17
也可以引入 java.lang.reflect.Modifier,通过 Modifier.toString() 方法转换为字符串,或者也可以直接使用 Modifier 的静态方法。
Modifier.toString(clazz.getModifiers()); // public final
Modifier.isPublic(clazz.getModifiers()); // true
Modifier.isPrivate(clazz.getModifiers()); // false
Modifier.isProtected(clazz.getModifiers()); // false
Modifier.isStatic(clazz.getModifiers()); // false
Modifier.isFinal(clazz.getModifiers()); // true
Modifier.isSynchronized(clazz.getModifiers()); // false
Modifier.isVolatile(clazz.getModifiers()); // false
Modifier.isNative(clazz.getModifiers()); // false
Modifier.isInterface(clazz.getModifiers()); // false
Modifier.isAbstract(clazz.getModifiers()); // false
类的类型
常用的有是否是接口、是否是枚举、是否是注解、是否是基本数据类型、是否是数组等。
clazz.isAnnotation(); // false
clazz.isArray(); // false
clazz.isEnum(); // false
clazz.isInterface(); // false
clazz.isPrimitive(); // false
clazz.isRecord(); // false
字段
这里有四个方法可以获取类的字段:
Field[] getFields():获取所有public的字段,包括父类的。Field[] getDeclaredFields():获取所有字段,包括private的,但不包括父类的。Field getField(String name):获取指定名称的public字段,包括父类的。Field getDeclaredField(String name):获取指定名称的字段,包括private的,但不包括父类的。
其中,Field 是 java.lang.reflect 包下的一个类。它有很多方法,比如:
getName():获取字段名。getType():获取字段类型,返回Class对象。getModifiers():获取修饰符。
Field field = clazz.getDeclaredField("value");
field.getDeclaringClass(); // class java.lang.String
field.getName(); // value
field.getType(); // class [B
field.getModifiers(); // 18
更多方法可以参考 Java doc。
可以使用 get(Object obj) 和 set(Object obj, Object value) 方法获取和设置字段的值。如果获取或修改的字段不是 public 的,需要先调用 setAccessible(true) 方法。
Field age = Person.class.getDeclaredField("age");
age.setAccessible(true); // 将 private 字段设置为可访问
Person person = new Person("Alice", 18);
Object value = age.get(person); // 18
age.set(person, 14); // 设置字段值
对于静态字段,可以传入 null 作为参数。
Field count = Person.class.getDeclaredField("count");
count.setAccessible(true);
int value = count.getInt(null); // 0
count.setInt(null, 1); // 设置字段值
需要注意的是,如果 JVM 运行期存在 SecurityManager,那么可能会不允许对 java 开头的包执行 setAccessible(true) 方法。
方法
方法和字段类似,有四个方法可以获取类的方法:
Method[] getMethods():获取所有public的方法,包括父类的。Method[] getDeclaredMethods():获取所有方法,包括private的,但不包括父类的。Method getMethod(String name, Class<?>... parameterTypes):获取指定名称和参数类型的public方法,包括父类的。Method getDeclaredMethod(String name, Class<?>... parameterTypes):获取指定名称和参数类型的方法,包括private的,但不包括父类的。
注意,这里的 getMethod 和 getDeclaredMethod 需要传入参数类型,这是因为 Java 中可以有多个方法名相同但参数类型不同的方法。
其中,Method 是 java.lang.reflect 包下的一个类。它有很多方法,比如:
getName():获取方法名。getReturnType():获取返回值类型,返回Class对象。getParameterTypes():获取参数类型,返回Class[]对象。getModifiers():获取修饰符。
更多方法可以参考 Java doc。
可以使用 invoke(Object obj, Object... args) 方法调用方法。如果调用的方法不是 public 的,需要先调用 setAccessible(true) 方法。
Method greeting = Person.class.getDeclaredMethod("greeting", String.class);
greeting.setAccessible(true);
Person person = new Person("Alice", 18);
String res = (String) greeting.invoke(person, "evening"); // Good evening, Alice!
构造方法
构造方法和字段、方法类似,有四个方法可以获取类的构造方法:
Constructor<?>[] getConstructors():获取所有public的构造方法。Constructor<?>[] getDeclaredConstructors():获取所有构造方法,包括private的。Constructor<?> getConstructor(Class<?>... parameterTypes):获取指定参数类型的public构造方法。Constructor<?> getDeclaredConstructor(Class<?>... parameterTypes):获取指定参数类型的构造方法,包括private的。
如果我们想要给一个类创建一个实例,有两种方式:
-
可以直接使用
newInstance()方法。Object obj = clazz.newInstance();这种方式在新版本中已经被废弃,因为它只能调用
public的无参构造方法。 -
可以使用
Constructor类的newInstance(Object... initargs)方法。Constructor<?> constructor = clazz.getDeclaredConstructor(String.class, int.class); constructor.setAccessible(true); Object obj = constructor.newInstance("Alice", 18);
类关系
Class 类有很多方法可以获取类的关系,比如:
getSuperclass():获取父类,返回Class对象。getInterfaces():获取接口,返回Class[]对象。getGenericSuperclass():获取父类泛型类型,返回Type对象。getGenericInterfaces():获取接口泛型类型,返回Type[]对象。
如果要判断一个类是否是另一个类的子类,有两种方式:
-
使用
isAssignableFrom(Class<?> cls)方法,判断是否可以向上转型,即cls能否赋值给当前类。 -
使用
isinstanceof关键字,判断是否是当前类的实例。
类加载器
Class 类有一个 getClassLoader() 方法,可以获取类加载器。
ClassLoader loader = clazz.getClassLoader();
类加载器有三种:
Bootstrap ClassLoader:负责加载核心类库,是 JVM 自带的类加载器。Extension ClassLoader:负责加载扩展类库,是sun.misc.Launcher$ExtClassLoader类的实例。AppClassLoader:负责加载应用程序类,是sun.misc.Launcher$AppClassLoader类的实例。
我们也可以自定义类加载器,只需要继承 ClassLoader 类,并重写 findClass(String name) 方法。
public class MyClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 从文件或网络中加载类
}
}
动态代理
动态代理是 Java 中的一种设计模式,可以在运行时创建一个实现一组接口的代理类。Java 中的动态代理主要有两种方式:
-
JDK 动态代理:通过
java.lang.reflect.Proxy类实现。public interface MyInterface { String sayHello(String arg); } public class MyInterfaceImpl implements MyInterface { @Override public String sayHello(String arg) { return "Hello, " + arg; } } public class MyInvocationHandler implements InvocationHandler { private Object target; public MyInvocationHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("Before invoke"); Object res = method.invoke(target, args); System.out.println("After invoke"); return res; } } MyInvocationHandler handler = new MyInvocationHandler(new MyInterfaceImpl()); MyInterface proxy = (MyInterface) Proxy.newProxyInstance( MyInterface.class.getClassLoader(), new Class<?>[] { MyInterface.class }, handler ); String res = proxy.sayHello("Alice");然而,JDK 动态代理只能代理实现了接口的类。
-
CGLIB 动态代理:通过
net.sf.cglib.proxy.Enhancer类实现。这是一个第三方库,需要引入cglib包。public class MyClass { public String sayHello(String arg) { return "Hello, " + arg; } } public class MyMethodInterceptor implements MethodInterceptor { @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println("Before invoke"); Object res = proxy.invokeSuper(obj, args); System.out.println("After invoke"); return res; } } Enhancer enhancer = new Enhancer(); enhancer.setClassLoader(MyClass.class.getClassLoader()); enhancer.setSuperclass(MyClass.class); enhancer.setCallback(new MyMethodInterceptor()); MyClass proxy = (MyClass) enhancer.create(); String res = proxy.sayHello("Alice");CGLIB 动态代理可以代理没有实现接口的类。但它的原理是通过继承来实现的,因此无法代理
final类和方法。同时,它的性能比 JDK 动态代理要差。CGLIB 目前已经不在维护,推荐使用
Byte Buddy等其它库。
注解
Java 中的注解是一种特殊的接口,它可以用来为类、方法、字段等添加元数据。注解的定义和接口类似,只不过在前面加上了 @ 符号。
public @interface MyAnnotation {
String value() default "";
}
通常推荐使用 default 关键字为注解的属性设置默认值。
有些注解可以修饰其他注解,这种注解称为元注解。Java 中有如下几种常用的元注解:
@Retention
@Retention 用来指定注解的生命周期,有三种取值:
-
RetentionPolicy.SOURCE注解只在源码中存在,编译时会被忽略。也就是说,这个注解被用来帮助开发者理解代码,而不会对代码产生任何影响。
-
RetentionPolicy.CLASS注解在源码和字节码中存在,运行时会被忽略。也就是说,这个注解会帮助编译器生成字节码,但在运行时不会被 JVM 读取。
-
RetentionPolicy.RUNTIME注解在源码、字节码和运行时都存在。也就是说,在 JVM 运行时,可以通过反射获取这个注解。
@Retention 的默认值是 RetentionPolicy.CLASS,即:
@Retention(RetentionPolicy.CLASS)
public @interface MyAnnotation {
String value() default "";
}
@Target
@Target 用来指定注解可以修饰的目标,有多种取值:
ElementType.ANNOTATION_TYPE:可以修饰注解。ElementType.CONSTRUCTOR:可以修饰构造方法。ElementType.FIELD:可以修饰字段。ElementType.LOCAL_VARIABLE:可以修饰局部变量。ElementType.METHOD:可以修饰方法。ElementType.PACKAGE:可以修饰包。ElementType.PARAMETER:可以修饰参数。ElementType.TYPE:可以修饰类、接口、枚举。
@Target 的默认值是 ElementType.TYPE。
@Target 还可以指定多个目标,比如:
@Target({
ElementType.TYPE,
ElementType.METHOD
})
public @interface MyAnnotation {
String value() default "";
}
@Repeatable
@Repeatable 用来指定注解可以重复修饰一个目标。
@Repeatable(MyAnnotations.class)
public @interface MyAnnotation {
String value() default "";
}
public @interface MyAnnotations {
MyAnnotation[] value();
}
@MyAnnotation("A")
@MyAnnotation("B")
public class MyClass {
}
@Inherited
@Inherited 用来指定注解可以被继承。
@Inherited
public @interface MyAnnotation {
String value() default "";
}
@MyAnnotation("A")
public class Parent {
}
public class Child extends Parent {
} // Child 也会被 @MyAnnotation 修饰
@Documented
@Documented 用来指定注解可以被 javadoc 工具读取。
@Documented
public @interface MyAnnotation {
String value() default "";
}
使用注解
使用注解时,需要在目标前面加上 @ 符号。
@MyAnnotation("Hello")
public class MyClass {
}
可以通过反射获取注解。
if (MyClass.class.isAnnotationPresent(MyAnnotation.class)) {
MyAnnotation annotation = MyClass.class.getAnnotation(MyAnnotation.class);
annotation.value(); // Hello
}
多线程
详见 Java 多线程。
Stream
Java 8 引入了 Stream 类,它可以用来处理集合类。
List<String> list = List.of("A", "B", "C");
Stream<String> stream = list.stream();
Stream 类提供了一系列的方法,可以对集合进行操作。
-
filter:过滤元素。Stream<String> stream = list.stream().filter(s -> s.equals("A")); -
map:映射元素。Stream<String> stream = list.stream().map(s -> s.toLowerCase()); -
distinct:去重。Stream<String> stream = list.stream().distinct(); -
limit:限制元素数量。Stream<String> stream = list.stream().limit(2);这会返回前两个元素。
skip:跳过元素。Stream<String> stream = list.stream().skip(2);这会跳过前两个元素。
-
sorted:排序。Stream<String> stream = list.stream().sorted(); -
forEach:遍历元素。list.stream().forEach(System.out::println); -
collect:收集元素。List<String> res = list.stream().collect(Collectors.toList()); -
reduce:合并元素。Optional<String> res = list.stream().reduce("", (s1, s2) -> s1 + s2);这会将所有元素合并成一个字符串。
-
anyMatch:判断是否有元素匹配。boolean res = list.stream().anyMatch(s -> s.equals("A"));allMatch:判断是否所有元素匹配。boolean res = list.stream().allMatch(s -> s.equals("A"));noneMatch:判断是否没有元素匹配。boolean res = list.stream().noneMatch(s -> s.equals("A")); -
findFirst:获取第一个元素。Optional<String> res = list.stream().findFirst(); -
count:获取元素数量。long res = list.stream().count(); -
min:获取最小值。Optional<String> res = list.stream().min(Comparator.naturalOrder()); -
max:获取最大值。Optional<String> res = list.stream().max(Comparator.naturalOrder()); -
flatMap:扁平化处理。List<List<String>> lists = List.of(List.of("A", "B"), List.of("C", "D")); List<String> res = lists.stream().flatMap(Collection::stream).collect(Collectors.toList());这会将二维数组扁平化为一维数组。
-
groupingBy:分组。Map<String, List<String>> res = list.stream().collect(Collectors.groupingBy(s -> s));这会将元素按照相同的值分组。
-
joining:连接。String res = list.stream().collect(Collectors.joining(","));这会将元素用
,连接起来。
Maven
Maven 是一个项目管理工具,可以用来构建、发布、文档、报告等。在安装 IDEA 时,Maven 会自动安装。当然,你也可以在 Maven 官网 下载。你可以使用下面的命令检查 Maven 是否安装成功:
mvn -v
通常,Maven 项目的目录结构如下:
project/
├──src/
│ ├──main/
│ │ ├──java/
│ │ └──resources/
│ └──test/
│ ├──java/
│ └──resources/
├──target/
├──pom.xml
└──...
src/main/java:存放主代码。src/main/resources:存放主代码的资源文件。src/test/java:存放测试代码。src/test/resources:存放测试代码的资源文件。target:存放编译后的文件。pom.xml:Maven 的配置文件。
Maven 的配置文件是一个 XML 文件,主要包括以下几个部分:
<project>
<modelVersion>4.0.0</modelVersion> <!-- 模型版本 -->
<groupId>com.example</groupId> <!-- 组织 ID -->
<artifactId>my-project</artifactId> <!-- 项目 ID -->
<version>1.0.0</version> <!-- 版本号 -->
<packaging>jar</packaging> <!-- 打包方式 -->
<name>My Project</name> <!-- 项目名称 -->
<description>This is my project.</description> <!-- 项目描述 -->
<properties> <!-- 属性 -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <!-- 编码 -->
<maven.compiler.release>21</maven.compiler.release> <!-- 编译版本 -->
</properties>
<dependencies> <!-- 依赖 -->
<dependency> <!-- 依赖项 -->
<groupId>org.slf4j</groupId> <!-- 组织 ID -->
<artifactId>slf4j-api</artifactId> <!-- 项目 ID -->
<version>2.1.0</version> <!-- 版本号 -->
<scope>compile</scope> <!-- 作用域 -->
</dependency>
</dependencies>
</project>
正如 Node、PIP 等包管理工具,Maven 的包也由三部分组成:组织 ID、项目 ID 和版本号。Maven 会下载这些包,并将它们放在本地仓库中。
用组织 ID 和项目 ID 是个明智的举动,不会出现重要的包名被野鸡开发者提前占领的情况。说的就是你,Go!
在引入依赖时,还有一个 scope 属性。它有以下几种取值:
compile:默认值,编译时需要。provided:编译时不需要,运行时需要。runtime:运行时需要,编译时不需要。test:测试时需要,编译和运行时不需要。
仓库
Maven 同样也有中央仓库(官方仓库)和镜像仓库、私有仓库之分。默认情况下,Maven 会从中央仓库下载包。你可以在 ~/.m2/settings.xml 文件中配置镜像仓库。
<mirrors>
<mirror>
<id>aliyun</id>
<name>aliyun</name>
<mirrorOf>central</mirrorOf>
<url>https://maven.aliyun.com/repository/central</url>
</mirror>
</mirrors>
要想使用私有仓库,可以在 pom.xml 文件中配置。
<repositories>
<repository>
<id>my-repo</id>
<name>My Repository</name>
<url>http://my-repo.com/maven2</url>
</repository>
</repositories>
需要注意,Maven 包的搜索顺序是:本地仓库、中央仓库、镜像仓库、私有仓库。如果你想要使用私有仓库中的一个包,但不巧的是,中央仓库有一个同名的包,那么你需要在 pom.xml 文件中指定私有仓库的 ID。
<dependency>
<groupId>com.example</groupId>
<artifactId>my-package</artifactId>
<version>1.0.0</version>
<scope>compile</scope>
<repositories>
<repository>
<id>my-repo</id>
<url>http://my-repo.com/maven2</url>
</repository>
</repositories>
</dependency>
Maven 相比于其它包管理工具,最烂的一点是,不支持自动引入包。你需要先去 mvnrepository 查找包,然后把 ID 和版本号填入 pom.xml 文件中。这种行为简直反人类。
模块
当项目有多个模块时,子模块可以使用 parent 标签指定父模块,这样就可以继承父模块引入的依赖。
<parent>
<groupId>com.example</groupId>
<artifactId>my-parent</artifactId>
<version>1.0.0</version>
</parent>
父模块可以使用 modules 标签指定子模块,这样就可以一次性构建所有模块。
<modules>
<module>my-module</module>
</modules>
生命周期
Maven 有三个生命周期:clean、default 和 site,每个生命周期包含多个阶段。
当我们执行 mvn clean 时,Maven 会执行 clean 生命周期的所有阶段。当我们执行 mvn package 时,Maven 会执行 default 生命周期的所有阶段。
而当我们执行 mvn compile 时,Maven 会执行 default 生命周期,从头开始直到 compile 阶段。
对于每个阶段,他会触发一系列行为。这些行为被称为插件目标。比如,compile 阶段会触发 maven-compiler-plugin 插件的 compile 目标。
我们可以在 pom.xml 文件中配置插件。
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>4.0.0</version>
<configuration>
<source>21</source>
<target>21</target>
</configuration>
</plugin>
</plugins>
</build>
Comments