分类
Java

Java无头模式

译者注

原文:

https://www.baeldung.com/java-headless-mode

Demo

https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-lang-2

1. 前言

有时,我们需要在没有真实的显示器、键盘、鼠标的情况下,来实现基于图形的Java应用程序,也就是说,程序运行在服务器或容器上。

本文我们将会学习Java的无头模式,以便实现上面提到的需求。我们将会知道,在无头模式的情况下,我们可以做什么、不能做什么。

2. 设置无头模式

在Java中,我们有很多方式来设置无头模式:

  • 将系统属性java.awt.headless设置为true
  • 使用命令行参数java -Djava.awt.headless=true
  • 在服务的启动脚本中,添加参数-Djava.awt.headless=trueJAVA_OPTS环境变量中

如果环境设置为无头模式,JVM就会识别到它。然而这样就会有一些细微的区别。我们来具体看一下。

3. 无头模式的UI组件示例

无头环境中的UI组件一个典型使用情况,就是作为图像转换器程序。
尽管它在运行过程中需要图形数据,但他并不需要显示。这样的app将会运行在服务器中,转换后的数据将会保存并通过网络传输给另一台机器来显示。

我们看一下如何操作。

首先,我们在JUnit类中启用无头模式,

@Before
public void setUpHeadlessMode() {
    System.setProperty("java.awt.headless", "true");
}

为了确保无头模式已经正常开启,我们可以写一个测试程序,通过调用java.awt.GraphicsEnvironment来断言无头模式是true:

@Test
public void whenSetUpSuccessful_thenHeadlessIsTrue() {
    assertThat(GraphicsEnvironment.isHeadless()).isTrue();
}

通过上面的测试方法,我们就可以准确的了解当前无头模式是否已经启用。
现在我们来做一个简单的图像转换器:

@Test
public void whenHeadlessMode_thenImagesWork() {
    boolean result = false;
    try (InputStream inStream = HeadlessModeUnitTest.class.getResourceAsStream(IN_FILE); 
      FileOutputStream outStream = new FileOutputStream(OUT_FILE)) {
        BufferedImage inputImage = ImageIO.read(inStream);
        result = ImageIO.write(removeAlphaChannel(inputImage), FORMAT, outStream);
    }

    assertThat(result).isTrue();
}

在下一个示例中,我们可以看到,所有字体的信息,包括字体规格都可用了:

@Test
public void whenHeadless_thenFontsWork() {
    GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
    String fonts[] = ge.getAvailableFontFamilyNames();
    assertThat(fonts).isNotEmpty();
    Font font = new Font(fonts[0], Font.BOLD, 14);
    FontMetrics fm = (new Canvas()).getFontMetrics(font);

assertThat(fm.getHeight()).isGreaterThan(0);
assertThat(fm.getAscent()).isGreaterThan(0);
assertThat(fm.getDescent()).isGreaterThan(0);
}

4. 无头异常HeadlessException

如果有组件依赖外围设备,它们就无法在无头模式中工作。当使用非交互环境时,就会抛出无头异常:

Exception in thread "main" java.awt.HeadlessException
    at java.awt.GraphicsEnvironment.checkHeadless(GraphicsEnvironment.java:204)
    at java.awt.Window.<init>(Window.java:536)
    at java.awt.Frame.<init>(Frame.java:420)

例如下面的代码,在一个无头模式的测试方法中,使用Frame对象就会导致无头异常:

@Test
public void whenHeadlessmode_thenFrameThrowsHeadlessException() {
    assertThatExceptionOfType(HeadlessException.class).isThrownBy(() -> {
        Frame frame = new Frame();
        frame.setVisible(true);
        frame.setSize(120, 120);
    });
}

根据经验,需要注意的是,这些顶级组件(比如Frame或者Button)需要交互环境,否则就会抛出异常。如果无头模式没有开启,它们可能会取而代之的抛出非交互错误irrecoverable Error

5. 在无头模式中绕过重量级组件

在本小节中,我们来提出一个问题:

如果我们把一个带有GUI组件的代码分别在“有头的生产环境机器上”和“无头的代码分析服务器上”运行,会发生什么?

在上面的例子中,我们已经知道了重量级组件无法在服务器上运行,并且会抛出异常。
所有我们可以使用一个条件来达到目的:

public void FlexibleApp() {
    if (GraphicsEnvironment.isHeadless()) {
        System.out.println("Hello World");
    } else {
        JOptionPane.showMessageDialog(null, "Hello World");
    }
}

用这样的模式,我们可以创造一个灵活的程序,根据它所在的环境来自动调整行为。

6、 总结

通过不同的代码示例,我们了解了Java的无头模式和它的部分原理。在这篇文章中提供了兼容列表,列表中给出了无头模式中可以进行哪些操作。

和往常一样,你可以在Github上找到本文中的示例代码。

分类
Java

Java中的不可变对象

1. 概述

本文我们将讨论什么是不可变对象,在JAVA中如何创建一个不可变的对象,以及为何要把某些对象设置为不可变的,这样做又有什么好处。

2. 什么是不可变对象

顾名思意,不可变对象就是说对象一旦通过实例化产生,其所有的属性将永远保持初始值,不会改变。

这也意味着:一旦不可变对象实例化创建完毕,在其被回收前,我们可以自由调用对象上任意暴露的方法,而对象将时刻保持实例化的初始状态。

字符串类型String是比较典型的不可变类,通过该类实例化的字符串为不可变对象。这同时意味着,字符串对象一旦创建,无论我们调用该对象是任意方法,该字符串均不会发生变化:

        String a = "codedemo.club";
        String b = a.replace("codedemo", "yunzhi");➊

        System.out.println(a);
        System.out.println(b);

        Assertions.assertEquals("codedemo.club", a);➋
        Assertions.assertEquals("yunzhi.club", b);
  • ➊ 调用a对象的replace方法来替换a字符串中的特定内容。
  • ➋ 替换方法执行后,a的值并未发生变化,我们说a为不可变对象。
codedemo.club
yunzhi.club

字符串String类型上的replace方法虽然提供了字符串替换的功能,但由于String被设计为不可变类,所以replace被调用后虽然返回了替换后的字符串,但原字符串a并不发生变化。我们把字符串a称为不可变对象,把字符串a属的String类称为不可变类

3. Java中的 final 关键字

在尝试自定义不可变类之前,我们先简单了解一下Java中的final关键字。

在Java中,变量默认是可以改变的(变量:可以改变的量),这意味着变量定义完成后,我们可以随时改变变量的值。

但如果我们使用final关键字来声明变量,Java编译器则不允许我们在后面改变该变量的值。比如以下代码将在编译时发生编译错误:

        final String a = "codedemo.club";

        // 以下代码将发生编译错误
        a = "yunzhi.club";

编译错误如下:

java: cannot assign a value to final variable a

值得注意的final关键字的作用仅是:禁止改变变量的引用值。但并不能禁止我们通过调用对象上的方法来改变其内部属性值(状态值),比如:

        final List<String> strings = new ArrayList<>(); 
        Assertions.assertEquals(0, strings.size());

        // 你不能改变strings的引用值,否则将发生编译错误
        // strings = new ArrayList<>();

        // 但可以调用ArrayList的add方法来改变strings的状态
        strings.add("codedemo.club"); 
        Assertions.assertEquals(1, strings.size());

上述代码成功的向被声明为final类型的strings列表中增加了子元素,也就是说strings虽然被声明了final,但其状态仍然是可改变不稳定的。这是由于ArrayList类型并不是一个不可变类型造成的。

4. Java中创建不可变对象

在了解了如何避免变量被改变的方法后,下面我们尝试创建不可变对象。

不可变对象的原则是:无论外部如何调用对象提供的公有方法,该对象的状态均不会发生改变。

比如我们可以直接将类中的属性全部声明为final:

public class Student {
    final private String name;
    final private int age;

上述代码中我们使用了final关键字来声明了Student类中的所有属性,而且这些属性的类型为主类型或不可变类型,这保证了Stduent类为不可变类,由该类实例化的对象为不可变对象。

但如果Student类型再增加一个Clazz班级属性,则欲保证Student为不可变类型,则需要保证Calzz类同样为不可变类。

public class Student {
    final private String name;
    final private int age;
    // 此时Student是否为可变类取决于Clazz是否为可变类
    final private Clazz clazz;

大多数时候,我们都需要在类中定义多个属性以存储对象的各个状态,对于声明为final类型的属性而言,只能在对象实例化时为其赋值:

public class Student {
    final private String name;
    
    // 声明属性时赋初值
    final private int age = 12;

    // 此时Student是否为可变类取决于Clazz是否为可变类
    final private Clazz clazz;

    /**
     * 或在构造函数中为其赋值
     */
    public Student(String name, Clazz clazz) {
        this.name = name;
        this.clazz = clazz;
    }
    
    // 此处省略了getter方法。所有属性均为final,所以该类无setter方法。

值得注意的是虽然Student在此为不可变类型,但Java提供的反射机制是可以忽视该不可变性,从而改变不可变对象的。在实际的使用中,我们往往不会(也不应该)这么做。

5. 不可变对象的优势

由于不可变对象状态的稳定性,所以在多线程情况下,我们可以放心地将不可变对象在不同的线程间传递、共享,而可以完全忽略各个线程是如何、何时利用该不可变对象的。

同时,我们也可以放心地将同一不可变对象分别嵌入到其它多个对象(可能是不同类型的)中,从而实现多对象共享某一相同对象的目标。重要的是,在此共享过程中,我们完全不必担心该共享对象可能会发生改变。

总之:不可变对象一旦实例化完成,我们便可以放心的使用它,而不必担心该对象可能会发生变化。

6. 总结

不可变对象一旦创建便会一直保持当前的状态,所以说它们是线程安全以及没有副作用的。正是由于这些特性,不可变对象往往被更多用于多线程环境中。