Skip to content

Files

Latest commit

8814ef5 · Oct 11, 2021

History

History
1342 lines (983 loc) · 58.1 KB

File metadata and controls

1342 lines (983 loc) · 58.1 KB

十五、管理对象、字符串、时间和随机数

我们将在本章中讨论的类与前几章中讨论的 Java 集合和数组一起属于一组类(主要是 Java 标准库和 Apache Commons 中的实用程序),每个程序员都必须掌握这些类才能成为有效的编码者。它们还说明了各种具有指导意义的软件设计和解决方案,可以作为最佳编码实践的模式。

我们将介绍以下功能领域:

  • 管理对象
  • 管理字符串
  • 管理时间
  • 管理随机数

概述类的列表包括:

  • java.util.Objects
  • org.apache.commons.lang3.ObjectUtils
  • java.lang.String
  • org.apache.commons.lang3.StringUtils
  • java.time.LocalDate
  • java.time.LocalTime
  • java.time.LocalDateTime
  • java.lang.Math
  • java.util.Random

管理对象

您可能不需要管理数组,甚至可能不需要管理集合(至少在一段时间内),但您无法避免管理对象,这意味着您可能每天都要使用本节中描述的类。

尽管java.util.Objects类在 2011 年(随着 Java 7 的发布)被添加到 Java 标准库中,而ObjectUtils类自 2002 年以来一直存在于 Apache Commons 库中,但它们的使用增长缓慢。这可能部分是因为他们最初在 2003 年的ObjectUtils中只有六种方法,而在 2011 年的Objects中只有九种方法。然而,它们是非常有用的方法,可以使代码更可读,更健壮,更不容易出错。因此,为什么这些类从一开始就没有得到更多的使用仍然是个谜。我们希望您在第一个项目中立即开始使用它们。

类 java.util.Objects

Objects只有 17 个方法都是静态的。我们在上一章实现类Person时已经使用了其中的一些:

class Person implements Comparable<Person> {
    private int age;
    private String name;
    public Person(int age, String name) {
        this.age = age;
        this.name = name == null ? "" : name;
    }
    public int getAge(){ return this.age; }
    public String getName(){ return this.name; }
    @Override
    public int compareTo(Person p){
        int result = this.name.compareTo(p.getName());
        if (result != 0) {
            return result;
        }
        return this.age - p.getAge();
    }
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null) return false;
        if(!(o instanceof Person)) return false;
        Person person = (Person)o;
        return age == person.getAge() &&
               Objects.equals(name, person.getName()); //line 25
    }
    @Override
    public int hashCode(){
        return Objects.hash(age, name);
    }
    @Override
    public String toString() {
        return "Person{age=" + age + ", name=" + name + "}";
    }
}

我们在前面的方法equals()hashCode()中使用了类Objects。一切都很顺利。但是,请注意我们如何检查前面构造函数中的参数name。如果参数为null,我们为字段name分配一个空String值。我们这样做是为了避开第 25 行。另一种方法是使用 Apache Commons 库中的类ObjectUtils。我们将在下一节中演示它。类ObjectUtils的方法处理null值,不需要将null参数转换为空String

但首先,让我们回顾一下Objects类的方法。

equals()和 deepEquals()

我们广泛讨论了equals()方法的实现,但总是假设它是在非null对象obj上调用的,因此调用obj.equals(anotherObject)无法生成NullPointerException

然而,有时我们需要比较两个对象,ab,而其中一个或两个对象可以是null。以下是这种情况下的典型代码:

boolean equals(Object a, Object b) {
    return (a == b) || (a != null && a.equals(b));
}

这是boolean Objects.equals(Object a, Object b)方法的实际源代码。它允许使用equals(Object)方法比较两个对象,并处理其中一个或两个对象都是null的情况。

Objects类的另一个相关方法是boolean deepEquals(Object a, Object b)。以下是它的源代码:

boolean deepEquals(Object a, Object b) {
    if (a == b)
        return true;
    else if (a == null || b == null)
        return false;
    else
        return Arrays.deepEquals0(a, b);
}

如您所见,它基于Arrays.deepEquals(),我们在上一节中讨论过。这些方法的演示代码有助于理解差异:

Integer[] as1 = {1,2,3};
Integer[] as2 = {1,2,3};
System.out.println(Arrays.equals(as1, as2));        //prints: true
System.out.println(Arrays.deepEquals(as1, as2));    //prints: true

System.out.println(Objects.equals(as1, as2));        //prints: false
System.out.println(Objects.deepEquals(as1, as2));    //prints: true

Integer[][] aas1 = {{1,2,3},{1,2,3}};
Integer[][] aas2 = {{1,2,3},{1,2,3}};
System.out.println(Arrays.equals(aas1, aas2));       //prints: false
System.out.println(Arrays.deepEquals(aas1, aas2));   //prints: true

System.out.println(Objects.equals(aas1, aas2));       //prints: false
System.out.println(Objects.deepEquals(aas1, aas2));   //prints: true

在前面的代码中,Objects.equals(as1, as2)Objects.equals(aas1, aas2)返回false,因为数组不能重写类Object的方法equals(),并且通过引用而不是值进行比较。

方法Arrays.equals(aas1, aas2)返回false的原因相同:因为嵌套数组的元素是数组,并且通过引用进行比较。

总之,如果您想通过两个对象的字段值来比较两个对象ab,那么:

  • 如果不是数组且a不是null,则使用a.equals(b)
  • 如果它们不是数组,并且两个对象都可以是null,则使用Objects.equals(a, b)
  • 如果两者都可以是数组,并且都可以是null,则使用Objects.deepEquals(a, b)

也就是说,我们可以看到Objects.deepEquals()方法是最安全的方法,但这并不意味着你必须一直使用它。大多数情况下,您将知道比较的对象可以是null还是可以是数组,因此您也可以安全地使用其他equals()方法。

hash()和 hashCode()

方法hash()hashCode()返回的散列值通常用作使用集合(例如HashSet()在散列中存储对象)的密钥。Object超类中的默认实现基于内存中的对象引用。它为具有相同实例字段值的同一类的两个对象返回不同的哈希值。这就是为什么,如果您需要两个类实例具有相同状态的相同哈希值,那么使用以下方法之一覆盖默认的hashCode()实现是很重要的:

  • int hashCode(Object value):计算单个对象的哈希值
  • int hash(Object... values):计算一个对象数组的哈希值(参见我们在上一个示例的Person类中如何使用它)

请注意,这两个方法在用作方法Objects.hash()的单个元素输入数组时,会为同一对象返回不同的哈希值:

System.out.println(Objects.hash("s1"));           //prints: 3645
System.out.println(Objects.hashCode("s1"));       //prints: 3614

两种方法产生相同散列的唯一值是null

System.out.println(Objects.hash(null));      //prints: 0
System.out.println(Objects.hashCode(null));  //prints: 0

当用作单个 NOTNULL 参数时,相同的值具有从方法Objects.hashCode(Object value)Objects.hash(Object... values)返回的不同哈希值。值null产生从每个方法返回的相同散列值0

使用类Objects进行散列值计算的另一个优点是,它允许null值,而尝试调用null引用上的实例方法hashCode()会生成NullPointerException

isNull()和 nonNull()

这两种方法只是对布尔表达式obj == nullobj != null的简单包装:

  • boolean isNull(Object obj):返回与obj == null相同的值
  • boolean nonNull(Object obj):返回与obj != null相同的值

下面是演示代码:

String object = null;

System.out.println(object == null);           //prints: true
System.out.println(Objects.isNull(object));   //prints: true

System.out.println(object != null);           //prints: false
System.out.println(Objects.nonNull(object));  //prints: false

requirennull()

Objects类的以下方法检查第一个参数的值,如果值为null,则抛出NullPointerException或返回提供的默认值:

  • T requireNonNull(T obj):如果参数为null则抛出NullPointerException而不带消息:
      String object = null;
      try {
          Objects.requireNonNull(object);
      } catch (NullPointerException ex){
          System.out.println(ex.getMessage());  //prints: null
      }
  • T requireNonNull(T obj, String message):如果第一个参数为null则抛出NullPointerException并提供消息:
      String object = null;
      try {
          Objects.requireNonNull(object, "Parameter 'object' is null");
      } catch (NullPointerException ex){
          System.out.println(ex.getMessage());  
          //Parameter 'object' is null
      }
  • T requireNonNull(T obj, Supplier<String> messageSupplier):如果第一个参数为null,则返回所提供函数生成的消息;如果生成的消息或函数本身为null,则抛出NullPointerException
      String object = null;
      Supplier<String> msg1 = () -> {
          String msg = "Msg from db";
          //get the corresponding message from database
          return msg;
      };
      try {
          Objects.requireNonNull(object, msg1);
      } catch (NullPointerException ex){
          System.out.println(ex.getMessage());  //prints: Msg from db
      }
      Supplier<String> msg2 = () -> null;
      try {
          Objects.requireNonNull(object, msg2);
      } catch (NullPointerException ex){
          System.out.println(ex.getMessage());  //prints: null
      }
      Supplier<String> msg3 = null;
      try {
          Objects.requireNonNull(object, msg3);
      } catch (NullPointerException ex){
          System.out.println(ex.getMessage());  //prints: null
      }
  • T requireNonNullElse(T obj, T defaultObj):非空返回第一个参数值,非空返回第二个参数值,或抛出NullPointerException消息defaultObj
      String object = null;
      System.out.println(Objects.requireNonNullElse(object, 
                              "Default value"));   
                              //prints: Default value
      try {
          Objects.requireNonNullElse(object, null);
      } catch (NullPointerException ex){
          System.out.println(ex.getMessage());     //prints: defaultObj
      }
  • T requireNonNullElseGet(T obj, Supplier<? extends T> supplier):非空返回第一个参数值,非空返回所提供函数生成的对象,或抛出NullPointerException消息defaultObj
      String object = null;
      Supplier<String> msg1 = () -> {
          String msg = "Msg from db";
          //get the corresponding message from database
          return msg;
      };
      String s = Objects.requireNonNullElseGet(object, msg1);
      System.out.println(s);                //prints: Msg from db

      Supplier<String> msg2 = () -> null;
      try {
       System.out.println(Objects.requireNonNullElseGet(object, msg2));
      } catch (NullPointerException ex){
       System.out.println(ex.getMessage()); //prints: supplier.get()
      }
      try {
       System.out.println(Objects.requireNonNullElseGet(object, null));
      } catch (NullPointerException ex){
       System.out.println(ex.getMessage()); //prints: supplier
      }

检查索引()

以下方法组检查集合或数组的索引和长度是否兼容:

  • int checkIndex(int index, int length):如果提供的index大于length - 1则抛出IndexOutOfBoundsException
  • int checkFromIndexSize(int fromIndex, int size, int length):如果提供的index + size大于length - 1则抛出IndexOutOfBoundsException
  • int checkFromToIndex(int fromIndex, int toIndex, int length):如果提供的fromIndex大于toIndextoIndex大于length - 1则抛出IndexOutOfBoundsException

以下是演示代码:

List<String> list = List.of("s0", "s1");
try {
    Objects.checkIndex(3, list.size());
} catch (IndexOutOfBoundsException ex){
    System.out.println(ex.getMessage());  
                         //prints: Index 3 out-of-bounds for length 2
}
try {
    Objects.checkFromIndexSize(1, 3, list.size());
} catch (IndexOutOfBoundsException ex){
    System.out.println(ex.getMessage());  
                //prints: Range [1, 1 + 3) out-of-bounds for length 2
}

try {
    Objects.checkFromToIndex(1, 3, list.size());
} catch (IndexOutOfBoundsException ex){
    System.out.println(ex.getMessage());  
                    //prints: Range [1, 3) out-of-bounds for length 2
}

比较()

Objects的方法int compare(T a, T b, Comparator<T> c)使用提供的比较器方法compare(T o1, T o2)来比较两个对象。在讨论集合排序时,我们已经描述了compare(T o1, T o2)方法的行为,因此应该预期以下结果:

int diff = Objects.compare("a", "c", Comparator.naturalOrder());
System.out.println(diff);  //prints: -2
diff = Objects.compare("a", "c", Comparator.reverseOrder());
System.out.println(diff);  //prints: 2
diff = Objects.compare(3, 5, Comparator.naturalOrder());
System.out.println(diff);  //prints: -1
diff = Objects.compare(3, 5, Comparator.reverseOrder());
System.out.println(diff);  //prints: 1

正如我们已经提到的,compare(T o1, T o2)方法返回String对象的排序列表中的对象o1o2的位置差,而只返回-101对象的位置差。API 描述为当对象相等时返回0,当第一个对象小于第二个对象时返回负数;否则,它将返回一个正数。

为了演示方法compare(T a, T b, Comparator<T> c)的工作原理,假设我们要对类Person的对象进行排序,以便名称和年龄分别按StringInteger类的自然顺序排列:

@Override
public int compareTo(Person p){
    int result = Objects.compare(this.name, p.getName(),
                                         Comparator.naturalOrder());
    if (result != 0) {
        return result;
    }
    return Objects.compare(this.age, p.getAge(), 
                                         Comparator.naturalOrder());
}

下面是Person类的compareTo(Object)方法新实现的结果:

Person p1 = new Person(15, "Zoe");
Person p2 = new Person(45, "Adam");
Person p3 = new Person(37, "Bob");
Person p4 = new Person(30, "Bob");
List<Person> list = new ArrayList<>(List.of(p1, p2, p3, p4));
System.out.println(list);//[{15, Zoe}, {45, Adam}, {37, Bob}, {30, Bob}]
Collections.sort(list);
System.out.println(list);//[{45, Adam}, {30, Bob}, {37, Bob}, {15, Zoe}] 

正如你所看到的,Person对象首先按其自然顺序按名称排序,然后按其自然顺序按年龄排序。例如,如果我们需要颠倒名称的顺序,我们将compareTo(Object)方法更改为:

@Override
public int compareTo(Person p){
    int result = Objects.compare(this.name, p.getName(),
                                         Comparator.reverseOrder());
    if (result != 0) {
        return result;
    }
    return Objects.compare(this.age, p.getAge(), 
                                         Comparator.naturalOrder());
}

结果与我们预期的一样:

Person p1 = new Person(15, "Zoe");
Person p2 = new Person(45, "Adam");
Person p3 = new Person(37, "Bob");
Person p4 = new Person(30, "Bob");
List<Person> list = new ArrayList<>(List.of(p1, p2, p3, p4));
System.out.println(list);//[{15, Zoe}, {45, Adam}, {37, Bob}, {30, Bob}]
Collections.sort(list);
System.out.println(list);//[{15, Zoe}, {30, Bob}, {37, Bob}, {45, Adam}] 

方法compare(T a, T b, Comparator<T> c)的缺点是它不处理null值。将new Person(25, null)对象添加到列表会在排序过程中触发NullPointerException。在这种情况下,最好使用org.apache.commons.lang3.ObjectUtils.compare(T o1, T o2)方法,我们将在下一节中演示。

toString()

在某些情况下,您需要将object(对某些类类型的引用)转换为其String表示形式。当参考obj被分配null值时(对象尚未创建),写入obj.toString()生成NullPointerException。对于此类情况,使用Objects类的以下方法是更好的选择:

  • String toString(Object o):非null时返回调用第一个参数toString()的结果,第一个参数值为null时返回调用null的结果
  • String toString(Object o, String nullDefault):当第一个参数不是null时返回调用toString()的结果,当第一个参数值为null时返回调用第二个参数值nullDefault

下面是演示如何使用这些方法的代码:

List<String> list = new ArrayList<>(List.of("s0 "));
list.add(null);
for(String e: list){
    System.out.print(e);                   //prints: s0 null
}
System.out.println();
for(String e: list){
    System.out.print(Objects.toString(e)); //prints: s0 null
}
System.out.println();
for(String e: list){
    System.out.print(Objects.toString(e, "element was null")); 
                                        //prints: s0 element was null
}

顺便说一下,与当前讨论无关,请注意我们是如何使用方法print()而不是println()在一行中显示所有结果的,因为方法print()没有添加行尾符号。

类 lang3.ObjectUtils

Apache Commons 库的类org.apache.commons.lang3.ObjectUtils补充了前面描述的类java.util.Objects的方法。本书的范围和分配的大小不允许对类ObjectUtils的所有方法进行详细审查,因此我们将简要描述它们,并按相关功能分组,仅演示那些与我们已经提供的示例一致的方法。

ObjectUtils课程的所有方法可分为七组:

  • 对象克隆方法:
    • T clone(T obj):如果提供的对象实现了Cloneable接口,则返回该对象的副本;否则返回null
    • T cloneIfPossible(T obj):如果提供的对象实现了Cloneable接口,则返回该对象的副本;否则,返回原始提供的对象。
  • 支持对象比较的方法:
    • int compare(T c1, T c2):比较实现接口Comparable的两个对象新排序的位置;允许任意一个或两个参数为null;将null值放在非空值前面。
    • int compare(T c1, T c2, boolean nullGreater):当参数nullGreater的值为false时,与前面的方法完全相同;否则,在非空值后面放置一个null值。我们可以在Person课程中使用后两种方法进行演示:
@Override
public int compareTo(Person p){
    int result = ObjectUtils.compare(this.name, p.getName());
    if (result != 0) {
        return result;
    }
    return ObjectUtils.compare(this.age, p.getAge());
}

此更改的结果允许我们为name字段使用null值:

Person p1 = new Person(15, "Zoe");
Person p2 = new Person(45, "Adam");
Person p3 = new Person(37, "Bob");
Person p4 = new Person(30, "Bob");
Person p5 = new Person(25, null);
List<Person> list = new ArrayList<>(List.of(p1, p2, p3, p4, p5));
System.out.println(list);  //[{15, Zoe}, {45, Adam}, {37, Bob}, {30, Bob}, {25, }]
Collections.sort(list);
System.out.println(list);  //[{25, }, {45, Adam}, {30, Bob}, {37, Bob}, {15, Zoe}]

由于我们使用了方法Objects.compare(T c1, T c2),所以将null值放在非空值前面。顺便问一下,您注意到我们不再显示null了吗?这是因为我们将Person类的toString()方法更改如下:

@Override
public String toString() {
    //return "{" + age + ", " + name + "}";
    return "{" + age + ", " + Objects.toString(name, "") + "}";
}

我们不只是显示字段name的值,而是使用Objects.toString(Object o, String nullDefault)方法,当对象为null时,用提供的nullDefault值替换对象。至于是否使用这种方法,在这种情况下,是风格问题。许多程序员可能会争辩说,我们必须显示实际值,而不能用它代替其他值。但是,我们这样做只是为了展示如何使用Objects.toString(Object o, String nullDefault)方法。

如果我们现在使用第二个compare(T c1, T c2, boolean nullGreater)方法,Person类的compareTo()方法如下:

@Override
public int compareTo(Person p){
    int result = ObjectUtils.compare(this.name, p.getName(), true);
    if (result != 0) {
        return result;
    }
    return ObjectUtils.compare(this.age, p.getAge());
}

然后,name设置为nullPerson对象将显示在排序列表的末尾:

Person p1 = new Person(15, "Zoe");
Person p2 = new Person(45, "Adam");
Person p3 = new Person(37, "Bob");
Person p4 = new Person(30, "Bob");
Person p5 = new Person(25, null);
List<Person> list = new ArrayList<>(List.of(p1, p2, p3, p4, p5));
System.out.println(list);  
               //[{15, Zoe}, {45, Adam}, {37, Bob}, {30, Bob}, {25, }]
Collections.sort(list);
System.out.println(list);  
               //[{45, Adam}, {30, Bob}, {37, Bob}, {15, Zoe}, {25, }]

并且,为了完成对null值的讨论,当null对象添加到列表中时,前面的代码将与NullPointerException中断:list.add(null)。为了避免异常,您可以使用一个特殊的Comparator对象来处理列表的null元素:

Person p1 = new Person(15, "Zoe");
Person p2 = new Person(45, "Adam");
Person p3 = new Person(37, "Bob");
Person p4 = new Person(30, "Bob");
Person p5 = new Person(25, null);
List<Person> list = new ArrayList<>(List.of(p1, p2, p3, p4, p5));
list.add(null);
System.out.println(list);  
        //[{15, Zoe}, {45, Adam}, {37, Bob}, {30, Bob}, {25, }, null]
Collections.sort(list, 
 Comparator.nullsLast(Comparator.naturalOrder()));
System.out.println(list);  
        //[{45, Adam}, {30, Bob}, {37, Bob}, {15, Zoe}, {25, }, null]

在这段代码中,您可以看到我们是如何表示希望看到列表末尾的null对象的。相反,我们可以使用另一个Comparator将空对象放置在排序列表的开头:

Collections.sort(list, 
                   Comparator.nullsFirst(Comparator.naturalOrder()));
System.out.println(list);  
        //[null, {45, Adam}, {30, Bob}, {37, Bob}, {15, Zoe}, {25, }]
  • notEqual
    • boolean notEqual(Object object1, Object object2):比较两个对象的不等性,其中一个或两个对象可以是null
  • identityToString
    • String identityToString(Object object):返回所提供对象的String表示,如同基类Object的默认方法toString()生成一样
    • void identityToString(StringBuffer buffer, Object object):追加所提供对象的String表示,如同基类Object的默认方法toString()生成一样
    • void identityToString(StringBuilder builder, Object object):追加所提供对象的String表示,如同基类Object的默认方法toString()生成一样
    • void identityToString(Appendable appendable, Object object):追加所提供对象的String表示,如同基类Object的默认方法toString()生成一样

以下代码演示了其中两种方法:

String s = "s0 " + ObjectUtils.identityToString("s1");
System.out.println(s);  //prints: s0 java.lang.String@5474c6c

StringBuffer sb = new StringBuffer();
sb.append("s0");
ObjectUtils.identityToString(sb, "s1");
System.out.println(s);  //prints: s0 java.lang.String@5474c6c
  • allNotNullanyNotNull

    • boolean allNotNull(Object... values):当提供的数组中的所有值都不是null时返回true
    • boolean anyNotNull(Object... values):当提供的数组中至少有一个值不是null时返回true
  • firstNonNulldefaultIfNull

    • T firstNonNull(T... values):返回所提供数组中非null的第一个值
    • T defaultIfNull(T object, T defaultValue):如果第一个参数为null,则返回提供的默认值
  • maxminmedianmode

    • T max(T... values):返回实现Comparable接口的已提供值的有序列表中的最后一个;仅当所有值均为null时返回null
    • T min(T... values):返回实现Comparable接口的已提供值的有序列表中的第一个;仅当所有值均为null时返回null
    • AUT0T0:返回在实现 Oracle T1 接口的提供的值的有序列表中间的值;如果值的计数是偶数,则返回中间的两个最小值。
    • AUT0T0:返回在所提供的值的列表中间的值,根据提供的 TUR1 T1 对象进行排序;如果值的计数是偶数,则返回中间的两个最小值。
    • T mode(T... items):返回所提供项目中出现频率最高的项目;返回null该项发生频率最高或没有一项发生频率最高时;下面是演示最后一种方法的代码:
String s = ObjectUtils.mode("s0", "s1", "s1");
System.out.println(s);     //prints: s1

s = ObjectUtils.mode("s0", "s1", "s2");
System.out.println(s);     //prints: null

s = ObjectUtils.mode("s0", "s1", "s2", "s1", "s2");
System.out.println(s);     //prints: null

s = ObjectUtils.mode(null);
System.out.println(s);     //prints: null

s = ObjectUtils.mode("s0", null, null);
System.out.println(s);     //prints: null

管理字符串

String类被大量使用。因此,您必须很好地处理它的功能。我们在第 5 章Java 语言元素和类型中已经谈到了String值不变性。我们已经证明,每次“修改”一个String值时,都会创建该值的一个新副本,这意味着在多次“修改”的情况下,会创建许多String对象,这会消耗内存并给 JVM 带来负担。

在这种情况下,建议使用类java.lang.StringBuilderjava.lang.StringBuffer,因为它们是可修改的对象,并且没有创建String值副本的开销。在本节的第一部分中,我们将展示如何使用它们,并解释这两个类之间的区别。

之后,我们将回顾类String的方法,然后提供类org.apache.commons.lang3.StringUtils的概述,它补充了类String的功能。

StringBuilder 和 StringBuffer

StringBuilderStringBuffer具有完全相同的方法列表。不同之处在于StringBuilder类的方法比StringBuffer类的方法执行得更快。这是因为StringBuffer类的开销不允许从不同的应用程序线程并发访问其值。因此,如果您不是为多线程处理编码,请使用StringBuilder

StringBuilderStringBuffer中有很多方法。但是,我们将展示如何仅使用append()方法,这是目前最流行的方法,用于需要修改多个String值的情况。其主要功能是在StringBuilder(或StringBuffer对象中已存储的值的末尾追加一个值。

方法append()对所有基元类型和类StringObjectCharSequenceStringBuffer重载,这意味着可以将这些类中任何一个传入对象的String表示追加到现有值。对于我们的演示,我们将只使用append(String s)版本,因为这可能是您大部分时间都要使用的。以下是一个例子:

List<String> list = 
  List.of("That", "is", "the", "way", "to", "build", "a", "sentence");
StringBuilder sb = new StringBuilder();
for(String s: list){
    sb.append(s).append(" ");
}
String s = sb.toString();
System.out.println(s);  //prints: That is the way to build a sentence

在类StringBuilder(和StringBuffer中还有方法replace()substring()insert()允许进一步修改值。但是,与方法append()相比,它们的使用频率要低得多,我们不打算讨论它们,因为它们超出了本书的范围。

类 java.lang.String

String有 15 个构造函数和近 80 个方法。对于这本书来说,谈论细节和演示每一种方法都太多了,所以我们将只对最流行的方法进行评论,只提及其余的方法。掌握了基础知识后,您可以阅读在线文档,了解如何使用String类的其他方法。

建设者

如果您担心应用程序创建的字符串占用太多内存,则类String的构造函数非常有用。问题在于String文本(例如abc)存储在内存中一个称为“字符串常量池”的特殊区域中,并且从不进行垃圾收集。这种设计背后的理念是String文字比数字消耗更多的内存。此外,处理这样大的实体会产生开销,这可能会对 JVM 造成负担。这就是为什么设计人员认为存储它们并在所有应用程序线程之间共享它们比分配新内存然后为相同的值清理几次要便宜的原因。

但是如果String值的重用率很低,而存储的String值消耗了太多内存,那么使用构造函数创建String对象可能是问题的解决方案。以下是一个例子:

String veryLongText = new String("asdakjfakjn akdb aakjn... akdjcnak");

以这种方式创建的String对象位于堆区域(所有对象都存储在这里),不再使用时会被垃圾收集。这就是String构造函数发光的时候。

如果需要,您可以使用类String的方法intern()在字符串常量池中创建 heapString对象的副本。它不仅允许我们与其他应用程序线程共享该值(在多线程处理中),还允许我们通过引用将其与另一个文本值进行比较(使用运算符==。如果引用相等,则表示它们指向池中相同的String值。

但是,主流程序员很少以这种方式管理内存,因此我们将不进一步讨论这个话题。

格式()

方法String format(String format, Object... args)允许将提供的对象插入字符串的指定位置,并根据需要对其进行格式化。类java.util.Formatter中有许多格式说明符。这里我们只演示%s,它通过在对象方法toString()上调用传入的对象,将其转换为其String表示:

String format = "There is a %s in the %s";
String s = String.format(format, "bear", "woods");
System.out.println(s); //prints: There is a bear in the woods

format = "Class %s is very useful";
s = String.format(format, new A());
System.out.println(s);  //prints: Class A is very useful

替换()

String值中的方法String replace(CharSequence target, CharSequence replacement)将第一个参数的值替换为第二个参数的值:

String s1 = "There is a bear in the woods";
String s2 = s1.replace("bear", "horse").replace("woods", "field");
System.out.println(s2);     //prints: There is a horse in the field

还有方法String replaceAll(String regex, String replacement)String replaceFirst(String regex, String replacement),它们具有类似的功能。

比较

我们已经在示例中使用了int compareTo(String anotherString)方法。它返回该String值和anotherString值在有序列表中的位置之差。它用于字符串的自然排序,因为它是Comparable接口的实现。

方法int compareToIgnoreCase(String str)执行相同的功能,但忽略比较字符串的大小写,不用于自然排序,因为它不是Comparable接口的实现。

价值(Objectj)

如果提供的对象是null,静态方法String valueOf(Object obj)返回null,或者对提供的对象调用方法toString()

valueOf(原语或字符[])

任何原语类型值都可以作为参数传递到静态方法String valueOf(primitive value),该方法返回所提供值的字符串表示形式。例如,String.valueOf(42)返回42。这组方法包括以下静态方法:

  • String valueOf(boolean b)
  • String valueOf(char c)
  • String valueOf(double d)
  • String valueOf(float f)
  • String valueOf(int i)
  • String valueOf(long l)
  • String valueOf(char[] data)
  • String valueOf(char[] data, int offset, int count)

copyValueOf(字符[])

方法String copyValueOf(char[] data)等同于valueOf(char[]),方法String copyValueOf(char[] data, int offset, int count)等同于valueOf(char[], int, int)。它们返回字符数组或其子数组的String表示。

方法void getChars(int srcBegin, int srcEnd, char[] dest, int dstBegin)将该String值中的字符复制到目标字符数组中。

indexOf()和子字符串()

各种int indexOf(String str)int lastIndexOf(String str)方法返回子字符串在字符串中的位置:

String s = "Introduction";
System.out.println(s.indexOf("I"));      //prints: 0
System.out.println(s.lastIndexOf("I"));  //prints: 0
System.out.println(s.lastIndexOf("i"));  //prints: 9
System.out.println(s.indexOf("o"));      //prints: 4
System.out.println(s.lastIndexOf("o"));  //prints: 10
System.out.println(s.indexOf("tro"));    //prints: 2

请注意,位置计数从零开始。

方法String substring(int beginIndex)返回字符串值的其余部分,从作为参数传入的位置(索引)开始:

String s = "Introduction";
System.out.println(s.substring(1));        //prints: ntroduction
System.out.println(s.substring(2));        //prints: troduction

具有beginIndex位置的字符是前一子字符串中出现的第一个字符。

方法String substring(int beginIndex, int endIndex)返回子字符串,从作为第一个参数传入的位置开始,返回作为第二个参数传入的位置:

String s = "Introduction";
System.out.println(s.substring(1, 2));        //prints: n
System.out.println(s.substring(1, 3));        //prints: nt

与方法substring(beginIndex)一样,具有beginIndex位置的字符是前一子串中出现的第一个字符,而不包括具有endIndex位置的字符。差值endIndex - beginIndex等于子串的长度。

这意味着以下两个子字符串相等:

System.out.println(s.substring(1));              //prints: ntroduction
System.out.println(s.substring(1, s.length()));  //prints: ntroduction

包含()和匹配项()

当提供的字符序列(子字符串)存在时,方法boolean contains(CharSequence s)返回true

String s = "Introduction";
System.out.println(s.contains("x"));          //prints: false
System.out.println(s.contains("o"));          //prints: true
System.out.println(s.contains("tro"));        //prints: true
System.out.println(s.contains("trx"));        //prints: false

其他类似方法包括:

  • boolean matches(String regex):使用正则表达式(非本书主题)
  • boolean regionMatches(int tOffset, String other, int oOffset, int length):比较两个字符串的区域
  • boolean regionMatches(boolean ignoreCase, int tOffset, String other, int oOffset, int length):同上,但有ignoreCase标志表示是否忽略该案例

split()、concat()和 join()

方法String[] split(String regex)String[] split(String regex, int limit)使用传入的正则表达式将字符串拆分为子字符串。本书中我们不解释正则表达式。但是,有一个非常简单的方法,即使您对正则表达式一无所知,也很容易使用:如果您只是将字符串中存在的任何符号或子字符串传递到该方法中,该字符串将被打断(拆分)为由传入值分隔的部分,例如:

String[] substrings = "Introduction".split("o");
System.out.println(Arrays.toString(substrings)); 
                                       //prints: [Intr, ducti, n]
substrings = "Introduction".split("duct");
System.out.println(Arrays.toString(substrings)); 
                                      //prints: [Intro, ion] 

这段代码只是演示了功能。但以下代码片段更实用:

String s = "There is a bear in the woods";
String[] arr = s.split(" ");
System.out.println(Arrays.toString(arr));  
                       //prints: [There, is, a, bear, in, the, woods]
arr = s.split(" ", 3);
System.out.println(Arrays.toString(arr));  
                          //prints: [There, is, a bear in the woods]

如您所见,split()方法中的第二个参数限制了生成的子字符串的数量。

方法String concat(String str)将传入的值添加到字符串的末尾:

String s1 =  "There is a bear";
String s2 =  " in the woods";
String s = s1.concat(s2);
System.out.println(s);  //prints: There is a bear in the woods

concat()方法创建一个新的String值,其结果是串联,因此非常经济。但是如果您需要添加(连接)许多值,那么使用StringBuilder(或者StringBuffer(如果您需要防止并发访问)将是一个更好的选择。我们在上一节中讨论了它。另一种选择是使用操作员+

String s =  s1 + s2;
System.out.println(s);  //prints: There is a bear in the woods

当与String值一起使用时,运算符+是基于StringBuilder实现的,因此允许通过修改现有值来添加String值。使用 StringBuilder 和仅由操作员+添加String值之间没有性能差异。

方法String join(CharSequence delimiter, CharSequence... elements)String join(CharSequence delimiter, Iterable<? extends CharSequence> elements)也是基于StringBuilder的。他们使用传入的delimiter将提供的值组合成一个String值,以分离创建的String结果中的组合值。以下是一个例子:

s = String.join(" ", "There", "is", "a", "bear", "in", "the", "woods");
System.out.println(s);  //prints: There is a bear in the woods

List<String> list = 
             List.of("There", "is", "a", "bear", "in", "the", "woods");
s = String.join(" ", list);
System.out.println(s);  //prints: There is a bear in the woods

startsWith()和 endsWith()之间

当字符串值以提供的子字符串prefix开始(或结束)时,以下方法返回true

  • boolean startsWith(String prefix)
  • boolean startsWith(String prefix, int toffset)
  • boolean endsWith(String suffix)

以下是演示代码:

boolean b = "Introduction".startsWith("Intro");
System.out.println(b);             //prints: true

b = "Introduction".startsWith("tro", 2);
System.out.println(b);             //prints: true

b = "Introduction".endsWith("ion");
System.out.println(b);             //prints: true

equals()和 equalsIgnoreCase()

我们已经多次使用了String类的boolean equals(Object anObject)方法,并指出它将该String值与其他对象进行比较。此方法仅当传入的对象为具有相同值的String时才返回true

方法boolean equalsIgnoreCase(String anotherString)的作用相同,但也忽略大小写,因此字符串AbCABC被视为相等。

contentEquals()和 copyValueOf()的值

方法boolean contentEquals(CharSequence cs)将该String值与实现接口CharSequence的对象的String表示进行比较。流行的CharSequence实现有CharBufferSegmentStringStringBufferStringBuilder

方法boolean contentEquals(StringBuffer sb)与之相同,但仅适用于StringBuffer。它的实现与contentEquals(CharSequence cs)略有不同,在某些情况下可能有一些性能优势,但我们不打算讨论这些细节。此外,当您对String值调用contentEquals()时,您可能甚至不会注意到这两种方法中使用了哪一种,除非您努力利用差异。

length()、isEmpty()和 hashCode()等

方法int length()返回String值中的字符数。 当String值中没有字符时,方法boolean isEmpty()返回true,方法length()返回零。

方法int hashCode()返回String对象的哈希值。

trim()、toLowerCase()和 toUpperCase()等

方法String trim()String值中删除前导空格和尾随空格。

以下方法更改String值中字符的大小写:

  • String toLowerCase()
  • String toUpperCase()
  • String toLowerCase(Locale locale)
  • String toUpperCase(Locale locale)

getBytes()、getChars()和 ToCharray()

以下方法将String值转换为字节数组,可以选择使用给定的字符集对其进行编码:

  • byte[] getBytes()
  • byte[] getBytes(Charset charset)
  • byte[] getBytes(String charsetName)

这些方法将所有的String值转换为其他类型,或仅转换其中的一部分:

  • IntStream chars()
  • char[] toCharArray()
  • char charAt(int index)
  • CharSequence subSequence(int beginIndex, int endIndex)

通过索引或流获取代码点

以下一组方法将所有String值或其一部分转换为其字符的 Unicode 码点:

  • IntStream codePoints()
  • int codePointAt(int index)
  • int codePointBefore(int index)
  • int codePointCount(int beginIndex, int endIndex)
  • int offsetByCodePoints(int index, int codePointOffset)

我们在第 5 章Java 语言元素和类型中解释了 Unicode 代码点。当您需要在char类型的两个字节中表示不适合的字符时,这些方法特别有用。此类字符的代码点大于Character.MAX_VALUE,即65535

lang3.StringUtils 类

ApacheCommons 库的类org.apache.commons.lang3.StringUtils有 120 多个静态实用程序方法,它们补充了我们在上一节中描述的类String的方法。

最流行的方法包括以下静态方法:

  • boolean isBlank(CharSequence cs):传入参数为空Stringnull或空白时返回true
  • boolean isNotBlank(CharSequence cs):传入参数不是空的Stringnull或空白时返回true
  • boolean isAlpha(CharSequence cs):传入参数仅包含 Unicode 字母时返回true
  • boolean isAlphaSpace(CharSequence cs):传入参数仅包含 Unicode 字母和空格(“”)时返回true
  • boolean isNumeric(CharSequence cs):传入参数只有位数时返回true
  • boolean isNumericSpace(CharSequence cs):传入参数仅包含数字和空格(“”)时返回true
  • boolean isAlphaNumeric(CharSequence cs):传入参数仅包含 Unicode 字母和数字时返回true
  • boolean isAlphaNumericSpace(CharSequence cs):传入参数仅包含 Unicode 字母、数字和空格(“”)时返回true

我们强烈建议您浏览这个类的 API,并了解其中的内容。

管理时间

java.time包及其子包中有许多类。它们是作为处理日期和时间的其他旧包的替代品引入的。新类是线程安全的(因此更适合多线程处理),而且同样重要的是,它们的设计更加一致,更易于理解。此外,对于日期和时间格式,新的实现遵循国际标准组织ISO),但也允许使用任何其他自定义格式。

我们将描述主要的五个类,并演示如何使用它们:

  • java.util.LocalDate
  • java.util.LocalTime
  • java.util.LocalDateTime
  • java.util.Period
  • java.util.Duration

所有这些,以及java.time包及其子包的其他类,都具有丰富的功能,涵盖了所有实际和任何可以想象的情况。但我们不打算涵盖所有这些,只介绍基本知识和最流行的用例。

java.time.LocalDate

课程LocalDate没有时间。它表示 ISO 8601 格式的日期,yyyy-MM-DD:

System.out.println(LocalDate.now());   //prints: 2018-04-14

如您所见,now()方法返回计算机上设置的当前日期:April 14, 2018是编写本节的日期。

同样,您可以使用静态方法now(ZoneId zone)获取任何其他时区的当前日期。ZoneId对象可以使用静态方法ZoneId.of(String zoneId)构造,其中String zoneId是方法ZonId.getAvailableZoneIds()返回的String值中的任意一个:

Set<String> zoneIds = ZoneId.getAvailableZoneIds();
for(String zoneId: zoneIds){
    System.out.println(zoneId);     
}

此代码打印许多时区 ID,其中一个是Asia/Tokyo。现在,我们可以在该时区找到现在的日期:

ZoneId zoneId = ZoneId.of("Asia/Tokyo");
System.out.println(LocalDate.now(zoneId));   //prints: 2018-04-15

LocalDate的对象也可以表示过去或将来的任何日期,使用以下方法:

  • LocalDate parse(CharSequence text):从 ISO 8601 格式的字符串构造对象,yyyy-MM-DD
  • LocalDate parse(CharSequence text, DateTimeFormatter formatter) :以对象DateTimeFormatter指定的格式从字符串构造一个对象,该对象有许多预定义的格式
  • LocalDate of(int year, int month, int dayOfMonth):以年、月、日为单位构造一个对象
  • LocalDate of(int year, Month month, int dayOfMonth):从年、月(作为enum常量)和日构造对象
  • LocalDate ofYearDay(int year, int dayOfYear):从一年中的一年和一天构造一个对象

以下代码演示了这些方法:

LocalDate lc1 =  LocalDate.parse("2020-02-23");
System.out.println(lc1);                     //prints: 2020-02-23

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
LocalDate lc2 =  LocalDate.parse("23/02/2020", formatter);
System.out.println(lc2);                     //prints: 2020-02-23

LocalDate lc3 =  LocalDate.of(2020, 2, 23);
System.out.println(lc3);                     //prints: 2020-02-23

LocalDate lc4 =  LocalDate.of(2020, Month.FEBRUARY, 23);
System.out.println(lc4);                     //prints: 2020-02-23

LocalDate lc5 = LocalDate.ofYearDay(2020, 54);
System.out.println(lc5);                     //prints: 2020-02-23

使用LocalDate对象,可以得到各种值:

System.out.println(lc5.getYear());          //prints: 2020
System.out.println(lc5.getMonth());         //prints: FEBRUARY
System.out.println(lc5.getMonthValue());    //prints: 2
System.out.println(lc5.getDayOfMonth());    //prints: 23

System.out.println(lc5.getDayOfWeek());     //prints: SUNDAY
System.out.println(lc5.isLeapYear());       //prints: true
System.out.println(lc5.lengthOfMonth());    //prints: 29
System.out.println(lc5.lengthOfYear());     //prints: 366

LocalDate对象可以修改:

System.out.println(lc5.withYear(2021));     //prints: 2021-02-23
System.out.println(lc5.withMonth(5));       //prints: 2020-05-23
System.out.println(lc5.withDayOfMonth(5));  //prints: 2020-02-05
System.out.println(lc5.withDayOfYear(53));  //prints: 2020-02-22

System.out.println(lc5.plusDays(10));       //prints: 2020-03-04
System.out.println(lc5.plusMonths(2));      //prints: 2020-04-23
System.out.println(lc5.plusYears(2));       //prints: 2022-02-23

System.out.println(lc5.minusDays(10));      //prints: 2020-02-13
System.out.println(lc5.minusMonths(2));     //prints: 2019-12-23
System.out.println(lc5.minusYears(2));      //prints: 2018-02-23 

LocalDate对象可以比较:

LocalDate lc6 =  LocalDate.parse("2020-02-22");
LocalDate lc7 =  LocalDate.parse("2020-02-23");
System.out.println(lc6.isAfter(lc7));       //prints: false
System.out.println(lc6.isBefore(lc7));      //prints: true

LocalDate类中还有许多其他有用的方法。如果您必须处理日期,我们建议您阅读这个类以及java.time包及其子包的其他类的 API。

java.time.LocalTime

LocalTime包含没有日期的时间。其方法类似于类LocalDate的方法。

下面是如何创建LocalTime类的对象:

System.out.println(LocalTime.now());         //prints: 21:15:46.360904

ZoneId zoneId = ZoneId.of("Asia/Tokyo");
System.out.println(LocalTime.now(zoneId));   //prints: 12:15:46.364378

LocalTime lt1 =  LocalTime.parse("20:23:12");
System.out.println(lt1);                     //prints: 20:23:12

LocalTime lt2 =  LocalTime.of(20, 23, 12);
System.out.println(lt2);                     //prints: 20:23:12

时间值的每个分量都可以从LocalTime对象中提取,如下所示:

System.out.println(lt2.getHour());          //prints: 20
System.out.println(lt2.getMinute());        //prints: 23
System.out.println(lt2.getSecond());        //prints: 12
System.out.println(lt2.getNano());          //prints: 0

可以修改对象:

System.out.println(lt2.withHour(3));        //prints: 03:23:12
System.out.println(lt2.withMinute(10));     //prints: 20:10:12
System.out.println(lt2.withSecond(15));     //prints: 20:23:15
System.out.println(lt2.withNano(300));      //prints: 20:23:12:000000300

System.out.println(lt2.plusHours(10));      //prints: 06:23:12
System.out.println(lt2.plusMinutes(2));     //prints: 20:25:12
System.out.println(lt2.plusSeconds(2));     //prints: 20:23:14
System.out.println(lt2.plusNanos(200));     //prints: 20:23:14:000000200

System.out.println(lt2.minusHours(10));      //prints: 10:23:12
System.out.println(lt2.minusMinutes(2));     //prints: 20:21:12
System.out.println(lt2.minusSeconds(2));     //prints: 20:23:10
System.out.println(lt2.minusNanos(200));     //prints: 20:23:11.999999800

LocalTime类的两个对象也可以比较:

LocalTime lt3 =  LocalTime.parse("20:23:12");
LocalTime lt4 =  LocalTime.parse("20:25:12");
System.out.println(lt3.isAfter(lt4));       //prints: false
System.out.println(lt3.isBefore(lt4));      //prints: true

LocalTime类中还有许多其他有用的方法。如果您需要花时间工作,我们建议您阅读这个类的 API 以及java.time包及其子包的其他类。

java.time.LocalDateTime

LocalDateTime包含日期和时间,并且具有类LocalDateLocalTime所具有的所有方法,因此我们不在这里重复它们。我们将仅展示如何创建LocalDateTime的对象:

System.out.println(LocalDateTime.now());  //2018-04-14T21:59:00.142804
ZoneId zoneId = ZoneId.of("Asia/Tokyo");
System.out.println(LocalDateTime.now(zoneId));  
                                   //prints: 2018-04-15T12:59:00.146038
LocalDateTime ldt1 =  LocalDateTime.parse("2020-02-23T20:23:12");
System.out.println(ldt1);                 //prints: 2020-02-23T20:23:12
DateTimeFormatter formatter = 
          DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss");
LocalDateTime ldt2 =  
       LocalDateTime.parse("23/02/2020 20:23:12", formatter);
System.out.println(ldt2);                 //prints: 2020-02-23T20:23:12
LocalDateTime ldt3 = LocalDateTime.of(2020, 2, 23, 20, 23, 12);
System.out.println(ldt3);                 //prints: 2020-02-23T20:23:12
LocalDateTime ldt4 =  
        LocalDateTime.of(2020, Month.FEBRUARY, 23, 20, 23, 12);
System.out.println(ldt4);                     //prints: 2020-02-23T20:23:12
LocalDate ld = LocalDate.of(2020, 2, 23);
LocalTime lt =  LocalTime.of(20, 23, 12);
LocalDateTime ldt5 = LocalDateTime.of(ld, lt);
System.out.println(ldt5);                     //prints: 2020-02-23T20:23:12

LocalDateTime类中还有许多其他有用的方法。如果您必须使用日期和时间,我们建议您阅读这个类以及java.time包及其子包的其他类的 API。

期间和持续时间

java.time.Periodjava.time.Duration被设计为包含一定的时间量:

  • Period对象包含以年、月和日为单位的时间量
  • Duration对象包含以小时、分钟、秒和纳秒为单位的时间量

下面的代码演示了它们在类LocalDateTime中的创建和使用,但在类LocalDate(用于Period)和LocalTime(用于Duration)中存在相同的方法:

LocalDateTime ldt1 = LocalDateTime.parse("2020-02-23T20:23:12");
LocalDateTime ldt2 = ldt1.plus(Period.ofYears(2));
System.out.println(ldt2); //prints: 2022-02-23T20:23:12

//The following methods work the same way:
ldt.minus(Period.ofYears(2));
ldt.plus(Period.ofMonths(2));
ldt.minus(Period.ofMonths(2));
ldt.plus(Period.ofWeeks(2));
ldt.minus(Period.ofWeeks(2));
ldt.plus(Period.ofDays(2));
ldt.minus(Period.ofDays(2));

ldt.plus(Duration.ofHours(2));
ldt.minus(Duration.ofHours(2));
ldt.plus(Duration.ofMinutes(2));
ldt.minus(Duration.ofMinutes(2));
ldt.plus(Duration.ofMillis(2));
ldt.minus(Duration.ofMillis(2));

下面的代码演示了创建和使用Period对象的其他一些方法:

LocalDate ld1 =  LocalDate.parse("2020-02-23");
LocalDate ld2 =  LocalDate.parse("2020-03-25");

Period period = Period.between(ld1, ld2);
System.out.println(period.getDays());       //prints: 2
System.out.println(period.getMonths());     //prints: 1
System.out.println(period.getYears());      //prints: 0
System.out.println(period.toTotalMonths()); //prints: 1

period = Period.between(ld2, ld1);
System.out.println(period.getDays());       //prints: -2

Duration的对象可以类似地创建和使用:

LocalTime lt1 =  LocalTime.parse("10:23:12");
LocalTime lt2 =  LocalTime.parse("20:23:14");
Duration duration = Duration.between(lt1, lt2);
System.out.println(duration.toDays());     //prints: 0
System.out.println(duration.toHours());    //prints: 10
System.out.println(duration.toMinutes());  //prints: 600
System.out.println(duration.toSeconds());  //prints: 36002
System.out.println(duration.getSeconds()); //prints: 36002
System.out.println(duration.toNanos());    //prints: 36002000000000
System.out.println(duration.getNano());    //prints: 0

PeriodDuration类中还有许多其他有用的方法。如果您需要花费大量的时间,我们建议您阅读java.time包及其子包的这些类和其他类的 API。

管理随机数

生成一个真正的随机数是一个不属于本书的大主题。但是对于绝大多数实际用途来说,Java 提供的伪随机数生成器已经足够好了,这就是我们将在本节中讨论的内容。

在 Java 标准库中,有两种主要的生成随机数的方法:

  • java.lang.Math.random()方法
  • java.util.Random

还有java.security.SecureRandom类,它提供了一个加密的强随机数生成器,但它不在入门课程的范围之内。

方法 java.lang.Math.random()

Math的静态方法double random()返回一个大于等于0.0小于1.0double类型值:

for(int i =0; i < 3; i++){
    System.out.println(Math.random());
    //0.9350483840148613
    //0.0477353019234189
    //0.25784245516898985
}

我们在前面的评论中捕捉到了结果。但在实践中,往往需要某个范围内的随机整数。为了满足这种需要,我们可以编写一个方法,例如,生成一个从 0(包括)到 10(不包括)的随机整数:

int getInteger(int max){
    return (int)(Math.random() * max);
}

以下是前一段代码的运行结果:

for(int i =0; i < 3; i++){
    System.out.print(getInteger(10) + " "); //prints: 2 5 6
}

如您所见,它生成一个随机整数值,可以是以下 10 个数字之一:0、1、…、9。下面是使用相同方法生成从 0(包括)到 100(不包括)的随机整数的代码:

for(int i =0; i < 3; i++){
    System.out.print(getInteger(100) + " "); //prints: 48 11 97
}

当您需要一个介于 100(含)和 200(不含)之间的随机数时,您可以将 100 添加到前面的结果中:

for(int i =0; i < 3; i++){
    System.out.print(100 + getInteger(100) + " "); //prints: 114 101 127
}

通过对生成的double值进行四舍五入,可以在结果中包括范围的两端:

int getIntegerRound(int max){
    return (int)Math.round(Math.random() * max);
}

当我们使用上述方法时,结果是:

for(int i =0; i < 3; i++){
    System.out.print(100 + getIntegerRound(100) + " "); //179 147 200
}

如您所见,范围的上限(数字 200)包含在可能的结果集中。只需将 1 添加到请求的上限范围即可实现相同的效果:

int getInteger2(int max){
    return (int)(Math.random() * (max + 1));
}

如果我们使用前面的方法,我们可以得到以下结果:

for(int i =0; i < 3; i++){
    System.out.print(100 + getInteger2(100) + " "); //167 200 132
}

但是如果你看一下Math.random()方法的源代码,你会发现它使用java.util.Random类及其nextDouble()方法来生成一个随机的双精度值。那么,让我们看看如何直接使用java.util.Random类。

类 java.util.Random

Random的方法doubles()生成一个大于或等于0.0且小于1.0double类型值:

Random random = new Random();
for(int i =0; i < 3; i++){
    System.out.print(random.nextDouble() + " "); 
    //prints: 0.8774928230544553 0.7822070124559267 0.09401796000707807 
}

我们可以使用前面章节中使用的方法nextDouble()相同。但当需要某个范围的随机整数值时,类还有其他方法可以使用,而无需创建自定义的getInteger()方法。例如,nextInt()方法返回一个介于Integer.MIN_VALUE(含)和Integer.MAX_VALUE(含)之间的整数值:

for(int i =0; i < 3; i++){
    System.out.print(random.nextInt() + " "); 
                        //prints: -2001537190 -1148252160 1999653777
}

使用带参数的相同方法,我们可以通过上限(排除)限制返回值的范围:

for(int i =0; i < 3; i++){
    System.out.print(random.nextInt(11) + " "); //prints: 4 6 2
}

此代码生成一个介于 0(含)和 10(含)之间的随机整数值。下面的代码返回一个介于 11(含 11)和 20(含 20)之间的随机整数值:

for(int i =0; i < 3; i++){
    System.out.print(11 + random.nextInt(10) + " "); //prints: 13 20 15
}

从该范围生成随机整数的另一种方式是使用ints(int count, int min, int max)方法返回的IntStream对象,其中count为请求值的个数,min为最小值(含),而max为最大值(不含):

String result = random.ints(3, 0, 101)
        .mapToObj(String::valueOf)
        .collect(Collectors.joining(" ")); //prints: 30 48 52

此代码返回从 0(含)到 100(含)的三个整数值。我们将在第 18 章流和管道中进一步讨论流。

练习–Objects.equals()结果

有三类:

public class A{}
public class B{}
public class Exercise {
    private A a;
    private B b;
    public Exercise(){
        System.out.println(java.util.Objects.equals(a, b));
    }
    public static void main(String... args){
        new Exercise();
    }
}

当我们运行Exercise类的main()方法时,将显示什么?ErrorFalseTrue

答复

显示屏将仅显示一个值:True。原因是私有字段ab都被初始化为null

总结

在本章中,我们向读者介绍了 Java 标准库和 Apache Commons 库中最流行的实用程序和一些其他类。为了成为一名有效的程序员,每个 Java 程序员都必须对自己的能力有深入的了解。研究它们还有助于了解各种软件设计模式和解决方案,这些模式和解决方案具有指导意义,可以作为任何应用程序中最佳编码实践的模式。

在下一章中,我们将向读者演示如何编写 Java 代码来操作数据库中的插入、读取、更新和删除数据。它还将简要介绍 SQL 和基本数据库操作。