Ch3nyang's blog collections_bookmark

post

person

about

category

category

local_offer

tag

rss_feed

rss

深入 Java | (1)
Java 进阶

calendar_month 2024-01
archive 编程
tag java

There are 4 posts in series 深入 Java.

本篇将涉及 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 的,但不包括父类的。

其中,Fieldjava.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 的,但不包括父类的。

注意,这里的 getMethodgetDeclaredMethod 需要传入参数类型,这是因为 Java 中可以有多个方法名相同但参数类型不同的方法。

其中,Methodjava.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 有三个生命周期:cleandefaultsite,每个生命周期包含多个阶段。

当我们执行 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

Share This Post