likes
comments
collection
share

【Java技术专题】「攻破技术盲区」带你攻破你很可能存在的Java技术盲点之技术功底指南(鲜为人知的技术)

作者站长头像
站长
· 阅读数 14

Java.lang包经常进行更新,主要涉及基本类型的包装类、进程管理和线程类。本章节内容的主要要点和方向: 【Java技术专题】「攻破技术盲区」带你攻破你很可能存在的Java技术盲点之技术功底指南(鲜为人知的技术)

基本类型的包装类

技术盲点:基本类型的比较

通常对于基本类型的比较我们都是用的是 == 或者 equals方法进行处理,但是在基本类型的比较方面,Boolean、Byte、Short、Integer、Long 和 Character 类都添加了一个静态 compare 方法,用于比较两个基本类型值的大小。

例如,Long 类的 compare 方法可以用来比较两个 long 类型的值。这个 compare 方法主要作为“语法糖”存在,可以简化进行基本类型数值比较时的代码。

如果需要对两个 int 数值 x 和 y 进行比较,一般的做法是使用代码“Integer.valueOf(x).compareTo(Integer.valueOf(y))”,而其实可以直接使用“Integer.compare(x, y)”来实现同样的功能。

技术盲点:字符串内部化(string interning)

对于字符串内部化(string interning)技术,开发人员可能并不陌生。采用这种技术是常见的优化策略,可以提高字符串比较时的性能,是一种典型的空间换时间的做法。而 Java 也采用了这种技术

在 Java 中,包含相同字符的字符串字面量引用的是相同的内部对象。此外,String 类还提供了 intern 方法,用于返回与当前字符串内容相同但已经被包含在内部缓存中的对象引用。当对被内部缓存的字符串进行比较时,可以直接使用“==”操作符,而无需使用更加耗时的 equals 方法。

字符串内部化的示例

public void stringIntern() {
	boolean test1= "test" == "test";
	boolean test2= (new String("test") == "test");
	boolean test3= (new String("test").intern() == "test");
}
  • 第一个字符串比较的结果为 true
  • 第二个字符串比较的结果为 false
  • 第三个字符串比较的结果为 true。

如果使用 equals 方法来进行第二个字符串的比较,结果也会是 true。

技术盲点:类型缓存机制(空间换时间)

String、Short和int、Byte等类型的扩展机制

根据Java语言规范,Java将字符串内部化机制扩展到了 -128 到 127 之间的数字。对于short类型和 int 类型在 -128 到 127 范围内的值,以及 char 类型在 \u0000 到 \u007f 范围内的值,它们对应的包装类对象始终指向相同的对象。因此,当通过“==”进行判断时,结果一定是 true。

valueOf方法的缓存触发

为了满足这个要求,Byte、Short 和 Integer 类的 valueOf 方法会对 -128 到 127 范围内的值进行内部缓存。而对于 Character 类,valueOf 方法会对 0 到 127 范围内的值进行内部缓存。

使用了内部缓存的示例

Integer 类的内部化的示例

public void numberCache() {
	boolean value1 = Integer.valueOf(3) == Integer.valueOf(3);
	boolean value2 = Integer.valueOf(129) == Integer.valueOf(129);
}

由于第一个比较操作的数值在 -128 到 127 之间,所以 Integer 类的 valueOf 方法会返回同一个缓存对象,因此 value1 的值为 true。而第二个比较操作的数值超出了默认的缓存范围,所以 valueOf 方法会返回两个不同的对象,因此 value2 的值为 false。

调整内部缓存范围

如果希望缓存更多的值,可以通过 Java 虚拟机启动参数"java.lang.Integer.IntegerCache.high"进行设置。例如,通过使用"-Djava.lang.Integer.IntegerCache.high=256",可以将数值缓存的范围扩大至-128到256。当重新运行上面的代码时,会发现value2的值变为true,因为129位于修改后的缓存范围内。


进程使用技术实战

Java标准API提供了创建运行于底层操作系统上的进程的能力。只需提供正确的命令和相关参数,即可启动一个进程。启动进程后,Java程序可以向进程输入数据,并读取进程生成的输出数据。

技术盲点:进程输入输出

在Java程序中启动其他进程时,最重要的是处理输入和输出。通常的做法是将Java程序的内部运行结果作为输入传递给新创建的进程,然后等待进程执行完成。在获取进程输出的运行结果后,再继续后续处理。通过这种方式,底层操作系统上的其他进程可以与Java程序很好地集成。对于Java程序来说,进程的输入是通过输出流传递给进程的。程序向输出流中写入的数据会通过管道传递给进程。进程的输出对于Java程序来说是通过输入流获取的。

技术盲点:ProcessBuilder处理进程

通过读取输入流的内容,可以获得进程的输出。标准的创建新进程的过程是使用java.lang.ProcessBuilder类来设置新进程的属性,然后通过start方法来启动进程的执行。

ProcessBuilder类的start方法返回一个表示进程的java.lang.Process类的对象。通过Process类的getOutputStream方法,可以获取用于向进程写入数据的输出流;而通过getInputStreamgetErrorStream方法,可以分别获取包含进程正常执行和出错时输出内容的输入流。

创建进程

下面是一个创建进程的示例代码:

public void startProcessNormal() throws IOException { 
	ProcessBuilder pb = new ProcessBuilder("cmd.exe", "/c", "netstat", "-a"); 
	Process process = pb.start();
	InputStream input = process.getInputStream();
	Files.copy(input, Paths.get("netstat.txt"), StandardCopyOption.REPLACE_EXISTING);
}

以上示例代码展示了如何在Windows上启动命令行工具,并执行"netstat -a"命令,将结果保存到一个文件中。

进程的输入和输出的继承式处理方式

Java又有了两种对于进程的输入和输出的处理方式。

  • 继承方式,即即新创建进程的输入和输出与当前的 Java 进程相同。
  • 文件式,即将文件作为进程的输入来源和输出目的地

继承方式

以下是使用继承方式的示例代码:

ProcessBuilder processBuilder = new ProcessBuilder("cmd.exe", "/c", "dir");
processBuilder.inheritIO();

Process process = processBuilder.start();

int exitCode = process.waitFor();
if (exitCode == 0) {
    System.out.println("进程执行成功");
} else {
    System.out.println("进程执行出错");
}

以上示例代码展示了如何启动一个进程,在Windows上使用命令行工具执行"dir"命令,并通过ProcessBuilder类的inheritIO方法将进程的输出设置为继承自父进程。运行结果会显示在Java程序默认的输出控制台中。

redirectOutput(Redirect.INHERIT)的方式
public void dir() throws IOException  { 
	ProcessBuilder pb =new ProcessBuilder("cmd.exe", "/c", "dir");
	pb.redirectOutput(Redirect.INHERIT);
	pb.start();
}

上面启动的进程通过 Windows 上的命令行工具来执行 dir 命令,通过 ProcessBuilder 类的 redirectOutput 方法把进程的输出 设置为继承自父进程,运行的结果就是 dir 命令的输出内容,会显示在 Java 程序默认的输出控制台中。

文件方式

如果需要将进程的或输出更改为文件,可以使用ProcessBuilder类中的redirectInputOutput方法其他重载形式。下面是基于文件的示例。

public void listProcesses() throws IOException { 
	ProcessBuilder pb = new ProcessBuilder("wmic", "process");
	File output = Paths.get("tasks.txt").toFile();
	pb.redirectOutput(output);
	pb.start();
}

上面展示了如将进的文件中。只需将一个java.io.File类对象作为Output方法的参数即可。这种方式当于管道方式读取输入流获取进程的输出,然后将其写入文件。通过标准API提供方式实现比自己编写实现要更简洁和可靠。


Thread类的特性分析

技术盲点:Thread是否可以clone以及参数校验

  • Thread类的 clone 方法改为始终抛出 NotSupportedException 异常。这因为对 Thread 类对象进行克隆是没有意义的。Java自身显式禁止了对 Thread 类对象的克隆操作。

  • Thread 类的 join 方法和 sleep 方法可以接收一个 long 类型的参数,表示等待的时间。然而,当这个参数值为负数时,并没有定义相应的处理方式。Java规定:如果这两个方法的等待时间参数的值为负数,则会抛出 IllegalArgumentException 异常。

技术盲点:Thread的线程的注意要点

在创建 Thread 类对象时,可以使用的参数包括:表示 Thread 类对象所在线程组的 java.lang.ThreadGroup 类的对象、表示需要运行的任务的 java.lang.Runnable 接口的现对象,以及表示线程名称的 String 类的对象。

  • ThreadGroup:如果传入的 ThreadGroup 类对象为 null,那么会先尝试调用当前配置好的安全管理器(java.lang.SecurityManager 类的对象)的 getThreadGroup 方法来获取 ThreadGroup 类对象;

  • Thread参数校验:如果没有配置安全管理器或 getThreadGroup 方法也返回 null,那么会使用当前线程线程组的 ThreadGroup 类对象。如果传入的 Runnable 接口的实现对象为 null,那么会调用 Thread 类对象本身的 run 方法。传入的线程名称为 null,会抛出 NullPointerException 异常。

  • setClassLoader:在调用 Thread 类的 setClassLoader 方法设置线程上下文类加载器时,如果传入的参数为 null,则表示使用系统类加载器。如果无使用系统类加载器,则使用启动类加载器。

同样地,如果当前线程的上下文类加载器是系统类加载器或启动类加载器,那么 getContextClassLoader 方法的返回值是 null


Objects对象操作

java.util 包中新增了一个用于操作对象的工具类java.util.ObjectsObjects 类中包含的都是静态方法,通过这些方法可以快速对对象进行操作对象的比较操作时,可以使用 Objectscompare 方法。

Objects的compare方法

在进行两个对象的比较操作时,可以使用 Objects 类的 compare 方法。一般来说,进行对象比较是先由 Java 类实现java.lang.Comparable 接口,再通过 compareTo 方法来进行比较。

Objects 类的 compare 方法的使用示例

一个简单的对 Long 对象进行比较的 Comparator接口的实现,以使用Objects类的compare` 方法的示例:

private static class ReverseComparator implements Comparator<Long> {
	public int compare(Long num1, Long num2) {
		return num2.compareTo(num1);
	}
}
public void compare() {
	int value1 = Objects.compare(1L, 2L, new ReverseComparator());
}

通过使用类的 compare 方法,我们可以方便地对 Long 对象进行比较,而无需手动处理 null 值或实现比较逻辑。

集合的排序

如果要对集合中的元素进行排序,还会使用到 java.util.Comparator 接口的实现。Objects 类的 compare 方法可以通过特定的 `` 接口的实现对象来比较两个对象。

import java.util.Comparator;
import java.util.Objects;

public class LongComparator implements Comparator<Long> {
    @Override
    public int compare(Long o1, Long o2) {
        return Objects.compare(o1, o2, Comparator.naturalOrder());
    }
}

public class Main {
    public static void main(String[] args)        
    	Long a = Long.valueOf(10);
        Long b = Long.valueOf(5);
        LongComparator comparator = new LongComparator();
        int result = comparator.compare(a, b);
        System.out.println(result);: 1
    }
}

Objects的equals方法

判断对象相等的方式般是调用 Object 类的 equals 方法。例如,要判断两个对象 a 和 b 是否相等,可以使用代码a.equals(b)。Objects类的equals方法可以直接判断两个对象是否相等,如Objects.equals(a,)。该方法的一个好处是会对 null值进行处理。

Objects的equals的判断逻辑

如果直接调用一个对象的equals方法,需要先判断该对象是否为null,而使用 Objects类的equals方法则不需要。调用Objects类的equals方法时,两个参数的值都是null,则判断结果是 true;而如果只有一个参数为 null,则判断结果是 false;如果两个参数都不为 null,则调用第一个参数的 equals` 方法进行判断。

Objects的deepEquals的判断逻辑

Objects 类中与 equals 方法作用相似的方法是 deepEquals 方法,利用该方法也可以对两个对象进行相等性判断。

不同之在于,如果 deepEquals 方法两个参数是数组,则会调用 java.util 类的deepEquals方法进行比较。 Arrays 类的 deepEquals 方法在进行数组比较时,会考虑数组中的所有元素的相等性。在其他情况下,deepEquals 方法和 equals 方法的作用是相同的。

Objects 类的 equals 方法的使用示例

public void equals() {
	boolean value1 = Objects.equals(new Object(), new Object()); 
	Object[] array1 = new Object[] {"Hello", 1, 1.0};
	Object[] array2 = new Object[] {"Hello", 1, 1.5};
	boolean value2 = Objects.deepEquals(array1, array2);
}

Objects的hashCode 方法

Objects 类中 hashCode 方法可以用来获取对象哈希值。如果参数为 null,则返回值是 0;否则返回值是参数对象的 hashCode 方法结果。

如果需要计算一组对象的哈希,可以使用 Objects 类的 hash 方法。Objects的 hash 方法的实现使用的是 Arrays 类中 hashCode 方法。

注意,调用 hash 方法传入单个对象作为的返回结果,并不相同于使用同的参数调用 hashCode 方法的结果。

Objects 类的 hash 和 hashCode 方法的使用示例

public void hash() {
	int hashCode1 = Objects.hashCode("Hello");
	int hashCode2 = Objects.hash("Hello", "World");
	int hashCode3 = Objects.hash("Hello");
}

Objects的toString方法

Objects类还提供了一不同重载形式的toString方法,用于获取对象的字符串表示。当参数为null时,toString方法返回null";而在其他情况下,相当于调用参数对象的toString方法。希望在参数为null时返回特定内容作为提示信息,可以使用toString方法的另一种重载形式,该形式通过额外的参数来指定参数值为null时的返回结果。

Objects 类的 toString 方法的使用示例

public void useToString() {
	String str1 = Objects.toString("Hello");
	String str2 = Objects.toString(null, " 空对象 ");
}