本篇将涉及 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