5、Java常用工具类(上)

Java String类

在前面的Java字符串小节,我们就已经接触了String类,但并未提及String类相关的操作,现在有了面向对象相关前置知识,我们知道了类下面可以有相关的操作,作为Java语言的内置类,String类也为我们预先定义了很多好用的方法,本小节我们将介绍String类的常用方法,并结合示例辅助理解。

1. String 对象的创建

String对象的创建有两种方式。

第1 种方式就是我们最常见的创建字符串的方式:

String str1 = "Hello, 夜猫编程";
代码块1

第 2 种方式是对象实例化的方式,使用new关键字,并将要创建的字符串作为构造参数:

String str2 = new String("Hello, Java");
代码块1

如果调用 String 类的无参构造方法,则会创建一个空字符串:

String str3 = new String();
代码块1

此处的str3就是一个空字符串。但注意,这种方式很少使用。

2. 获取字符串长度

可以使用length()方法来获取字符串的长度。例如:

实例演示

public class StringMethod1 {
  public static void main(String[] args) {
    // 创建String对象str
    String str = "hello world!";
    // 调用对象下length()方法,并使用int类型变量接收返回结果
    int length = str.length();
    System.out.println("str的长度为:" + length);
  }
}
123456789

可查看在线运行效果

运行结果:

str1的长度为:12
代码块1

注意,hello world!中的空格也算一个字符。

3. 字符串查找3.1 获取指定位置字符

可以使用char charAt(int index)方法获取字符串指定位置的字符。它接收一个整型的index参数,指的是索引位置,那什么是索引位置呢?例如,有一字符串I love Java,其每个字符的索引如下图所示:

可以从图示中看出,索引下标从0开始。假如我们要获取字符J,则为方法传入参数7即可:

实例演示

public class StringMethod2 {
  public static void main(String[] args) {
    String str = "I love Java";
    char c = str.charAt(7);
    System.out.println("索引位置为7的字符为:" + c);
  }
}
1234567

可查看在线运行效果

运行结果:

索引位置为7的字符为:J
代码块1
3.2 查找字符串位置

这里介绍查找字符串位置的两个方法:

indexOf() 获取字符或子串在字符串中第一次出现的位置。

lasIndexOf() 获取字符或子串在字符串中最后一次出现的位置。

这里的子串指的就是字符串中的连续字符组成的子序列。例如,字符串Hello就是字符串Hello Java的子串。

indexOf()有多个重载方法,这里我们只演示其中最常用的两个。

获取字符在字符串中第一次出现的位置:

实例演示

public class StringMethod2 {
  public static void main(String[] args) {
    String str = "I love Java, I love imooc!";
    int i = str.indexOf('a');
    System.out.println("字符a在字符串str第一次出现的位置为:" + i);
  }
}
1234567

可查看在线运行效果

运行结果:

字符a在字符串str第一次出现的位置为:8
代码块1

获取子串在字符串中第一次出现的位置:

实例演示

public class StringDemo2 {
  public static void main(String[] args) {
    String str = "I love Java, I love imooc!";
    int i = str.indexOf("love");
    System.out.println("子串love在字符串str第一次出现的位置为:" + i);
  }
}
1234567

可查看在线运行效果

运行结果:

子串love在字符串str第一次出现的位置为:2
代码块1

关于lastIndexOf(),我们也只演示最常用的两个重载方法。

获取字符在字符串中最后一次出现的位置:

实例演示

public class StringMethod2 {
  public static void main(String[] args) {
    String str = "I love Java, I love imooc!";
    int i = str.lastIndexOf('e');
    System.out.println("字符e在字符串str最后一次出现的位置为:" + i);
  }
}
1234567

可查看在线运行效果

运行结果:

字符e在字符串str最后一次出现的位置为:18
代码块1

获取子串在字符串中最后一次出现的位置:

实例演示

public class StringMethod2 {
  public static void main(String[] args) {
    String str = "I love Java, I love imooc!";
    int i = str.lastIndexOf("I love");
    System.out.println("字串I love在字符串str最后一次出现的位置为:" + i);
  }
}
1234567

可查看在线运行效果

运行结果:

字串I love在字符串str最后一次出现的位置为:13
代码块1

需要特别注意的是,以上方法的参数都是区分大小写的。这也就意味着,你永远无法在I love Java中查找到字符E。如果没有查找,上述方法都会返回一个整型值:-1。我们来看以下示例:

实例演示

public class StringMethod2 {
  public static void main(String[] args) {
    String str = "I love Java";
    int i = str.indexOf('E');
    System.out.println(i);
  }
}
1234567

可查看在线运行效果

运行结果:

-1
代码块1
4. 字符串截取

字符串的截取也称为获取子串,在实际开发中经常用到,可以使用substring()方法来获取子串,String类中有两个重载的实例方法:

String substring(int beginIndex) 获取从beginIndex位置开始到结束的子串。

String substring(int beginIndex, int endIndex) 获取从beginIndex位置开始到endIndex位置的子串(不包含endIndex位置字符)。

关于这两个方法的使用,我们来看一个实例:

实例演示

public class StringMethod3 {
  public static void main(String[] args) {
    String str = "I love Java";
    String substring = str.substring(2);
    String substring1 = str.substring(2, 6);
    System.out.println("从索引位置2到结束的子串为:" + substring);
    System.out.println("从索引位置2到索引位置6的子串为:" + substring1);
  }
}
123456789

可查看在线运行效果

运行结果:

从索引位置2到结束的子串为:love Java
从索引位置2到索引位置6的子串为:love
代码块12

要特别注意,方法签名上有两个参数的substring(int beginIndex, int endIndex)方法,截取的子串不包含endIndex位置的字符。

5. 字符串切割5.1 切割为字串数组

String[] split(String regex)方法可将字符串切割为子串,其参数regex是一个正则表达式分隔符,返回字符串数组。例如,我们使用空格作为分隔符来切割I love Java字符串,结果将返回含有3个元素的字符串数组:

实例演示

public class StringMethod4 {
  public static void main(String[] args) {
    String str1 = "I love Java";
    // 将字符串str1以空格分隔,并将分割结果赋值给strArr数组
    String[] strArr = str1.split(" ");
    // 遍历数组,打印每一个元素
    for (String str: strArr) {
      System.out.print(str + '\t');
    }
    
  }
}
12345678910111213

可查看在线运行效果

运行结果:

I love Java 
代码块1

注意,有几种特殊的分隔符:* ^ : | . \,要使用转义字符转义。例如:

// 以*切割
String str2 = "I*love*Java";
String[] strArr2 = str2.split("\\*");
// 以\切割
String str3 = "I\\love\\Java";
String[] strArr4 = str3.split("\\\\");
// 以|切割
String str4 = "I|love|Java";
String[] strArr4 = str4.split("\\|");
代码块1234567891011

另外,还有一个重载方法String[] split(String regex, int limit),其第二个参数limit用以控制正则匹配被应用的次数,因此会影响结果的长度,此处不再一一举例介绍。

5.2 切割为 byte 数组

在实际工作中,网络上的数据传输就是使用二进制字节数据。因此字符串和字节数组之间的相互转换也很常用。

我们可以使用getBytes()方法将字符串转换为byte数组。例如:

实例演示

public class StringMethod4 {
  public static void main(String[] args) {
    String str2 = "我喜欢Java";
    System.out.println("将字符串转换为byte数组:");
    // 将字符串转换为字节数组
    byte[] ascii = str2.getBytes();
    // 遍历字节数组,打印每个元素
    for (byte aByte : ascii) {
      System.out.print(aByte + "\t");
    }
  }
}
123456789101112

可查看在线运行效果

运行结果:

将字符串转换为byte数组:
-26 -120 -111 -27 -106 -100 -26 -84 -94 74 97 118 97 
代码块12

将字节数组转换为字符串的方法很简单,直接实例化一个字符串对象,将字节数组作为构造方法的参数即可:

// 此处的ascii为上面通过字符串转换的字节数组
String s = new String(ascii);
代码块12
6. 字符串大小写转换

字符串的大小写转换有两个方法:

toLowerCase() 将字符串转换为小写

toUpperCase() 将字符串转换为大写

我们来看一个实例:

实例演示

public class StringMethod5 {
  public static void main(String[] args) {
    String str = "HELLO world";
    String s = str.toLowerCase();
    System.out.println("字符串str为转换为小写后为:" + s);
    String s1 = s.toUpperCase();
    System.out.println("字符串s为转换为大写后为:" + s1);
  }
}
123456789

可查看在线运行效果

运行结果:

字符串str为转换为小写后为:hello world
字符串s为转换为大写后为:HELLO WORLD
代码块12

试想,如果想把字符串HELLO world中的大小写字母互换,该如何实现呢?

这里可以结合字符串切割方法以及字符串连接来实现:

实例演示

public class StringMethod5 {
  public static void main(String[] args) {
    String str = "HELLO world";
    // 先切割为数组
    String[] strArr = str.split(" ");
    // 将数组中元素转换大小写并连接为一个新的字符串
    String result = strArr[0].toLowerCase() + " " + strArr[1].toUpperCase();
    System.out.println("字符串str的大小写互换后为:" + result);
  }
}
12345678910

可查看在线运行效果

运行结果:

字符串str的大小写互换后为:hello WORLD
代码块1

当然,实现方式不止一种,你可以结合所学写出更多的方式。

7. 字符串比较

String类提供了boolean equals(Object object)方法来比较字符串内容是否相同,返回一个布尔类型的结果。

需要特别注意的是,在比较字符串内容是否相同时,必须使用equals()方法而不能使用==运算符。我们来看一个示例:

实例演示

public class StringMethod6 {
  public static void main(String[] args) {
    // 用两种方法创建三个内容相同的字符串
    String str1 = "hello";
    String str2 = "hello";
    String str3 = new String("hello");
    System.out.println("使用equals()方法比较str1和str2的结果为:" + str1.equals(str2));
    System.out.println("使用==运算符比较str1和str2的结果为:" + (str1 == str2));
    System.out.println("使用==运算符比较str1和str2的结果为:" + (str1 == str2));
    System.out.println("使用==运算符比较str1和str3的结果为:" + (str1 == str3));
  }
}
123456789101112

可查看在线运行效果

运行结果:

使用equals()方法比较str1和str2的结果为:true
使用==运算符比较str1和str2的结果为:true
使用equals()方法比较str1和str3的结果为:true
使用==运算符比较str1和str3的结果为:false
代码块1234

代码中三个字符串str1,str2和str3的内容都是hello,因此使用equals()方法对它们进行比较,其结果总是为true。

注意观察执行结果,其中使用==运算符比较str1和str2的结果为true,但使用==运算符比较的str1和str3的结果为false。这是因为==运算符比较的是两个变量的地址而不是内容。

要探究其原因,就要理解上述创建字符串的代码在计算机内存中是如何执行的。下面我们通过图解的形式来描述这三个变量是如何在内存中创建的。

当执行String str1 = "hello;"语句时,会在内存的栈空间中创建一个str1,在常量池中创建一个"hello",并将str1指向hello。

当执行String str2 = "hello";语句时,栈空间中会创建一个str2,由于其内容与str1相同,会指向常量池中的同一个对象。所以str1与str2指向的地址是相同的,这就是==运算符比较str1和str2的结果为true的原因。

当执行String str3 = new String("hello");语句时,使用了new关键字创建字符串对象,由于对象的实例化操作是在内存的堆空间进行的,此时会在栈空间创建一个str3,在堆空间实例化一个内容为hello的字符串对象,并将str3地址指向堆空间中的hello,这就是==运算符比较str1和str3的结果为false的原因。

StringBuilder

上一节,我们学习了 Java 的 String 类,并介绍了其常用方法。本小节我们来介绍字符串的另外一个类:StringBuilder,我们将会了解到 StringBuilder 与 String 的差异,StringBuilder 的使用场景,也会介绍与 StringBuilder 类对应的 StringBuffer 类,StringBuilder 的使用方法以及其常用方法是本小节的重点学习内容。

1. StringBuilder 概述1.1 什么是 StringBuilder

与 String 相似,StringBuilder 也是一个与字符串相关的类,Java 官方文档给 StringBuilder 的定义是:可变的字符序列。

1.2 为什么需要 StringBuilder

在 Java 字符串的学习中,我们知道了字符串具有不可变性,当频繁操作字符串时候,会在常量池中产生很多无用的数据(回忆图示)。

而 StringBuilder 与 String 不同,它具有可变性。相较 String 类不会产生大量无用数据,性能上会大大提高。

因此对于需要频繁操作字符串的场景,建议使用 Stringbuilder 类来代替 String 类。

2. StringBuffer 概述2.1 定义

了解了 StringBuilder 类 ,StringBuffer 也是不得不提的一个类,Java 官方文档给出的定义是:线程安全的可变字符序列。

2.2 与前者的区别

StringBuffer 是 StringBuilder 的前身,在早期的 Java 版本中应用非常广泛,它是 StringBuilder 的线程安全版本(线程我们将在后面的小节中介绍),但实现线程安全的代价是执行效率的下降。

你可以对比 StringBuilder 和 StringBuffer 的接口文档,它们的接口基本上完全一致。为了提升我们代码的执行效率,在如今的实际开发中 StringBuffer 并不常用。因此本小节的重点在 StringBuilder 的学习。

3. StringBuilder 的常用方法3.1 构造方法

StringBuilder 类提供了如下 4 个构造方法:

StringBuilder() 构造一个空字符串生成器,初始容量为 16 个字符;

StringBuilder(int catpacity) 构造一个空字符串生成器,初始容量由参数 capacity 指定;

StringBuilder(CharSequence seq) 构造一个字符串生成器,该生成器包含与指定的 CharSequence 相同的字符。;

StringBuilder(String str) 构造初始化为指定字符串内容的字符串生成器。

其中第 4 个构造方法最为常用,我们可以使用 StringBuilder 这样初始化一个内容为 hello 的字符串:

StringBuilder str = new StringBuilder("Hello");
代码块1
3.2 成员方法

StringBuilder 类下面也提供了很多与 String 类相似的成员方法,以方便我们对字符串进行操作。下面我们将举例介绍一些常用的成员方法。

3.2.1 字符串连接

可以使用 StringBuilder 的 StringBuilder append(String str) 方法来实现字符串的连接操作。

我们知道,String 的连接操作是通过 + 操作符完成连接的:

String str1 = "Hello";
String str2 = "World";
String str3 = str1 + " " + str2;
代码块123

如下是通过 StringBuilder 实现的字符串连接示例:

实例演示

public class ConnectString1 {
  public static void main(String[] args) {
    // 初始化一个内容为 Hello 的字符串生成器
    StringBuilder str = new StringBuilder("Hello");
    // 调用append()方法进行字符串的连接
    str.append(" ");
    str.append("World");
    System.out.println(str);
  }
}
12345678910

可查看在线运行效果

运行结果:

Hello World
代码块1

由于 append() 方法返回的是一个 StringBuilder 类型,我们可以实现链式调用。例如,上述连续两个 append() 方法的调用语句,可以简化为一行语句:

str.append(" ").append("World");
代码块1

如果你使用 IDE 编写如上连接字符串的代码,可能会有下面这样的提示(IntelliJ idea 的代码截图):

提示内容说可以将 StringBuilder 类型可以替换为 String 类型,也就是说可以将上边地代码改为:

String str = "Hello" + " " + "World";
代码块1

这样写并不会导致执行效率的下降,这是因为 Java 编译器在编译和运行期间会自动将字符串连接操作转换为 StringBuilder 操作或者数组复制,间接地优化了由于 String 的不可变性引发的性能问题。

值得注意的是,append() 的重载方法有很多,可以实现各种类型的连接操作。例如我们可以连接 char 类型以及 float 类型,实例如下:

实例演示

public class ConnectString2 {
  public static void main(String[] args) {
    StringBuilder str = new StringBuilder("小明的身高为");
    str.append(':').append(172.5f);
    System.out.println(str);
  }
}
1234567

可查看在线运行效果

运行结果:

小明的身高为:172.5
代码块1

上面代码里连续的两个 append() 方法分别调用的是重载方法 StringBuilder append(char c) 和 StringBuilder append(float f)。

3.2.2 获取容量

可以使用 int capacity() 方法来获取当前容量,容量指定是可以存储的字符数(包含已写入字符),超过此数将进行自动分配。注意,容量与长度(length)不同,长度指的是已经写入字符的长度。

例如,构造方法 StringBuilder() 构造一个空字符串生成器,初始容量为 16 个字符。我们可以获取并打印它的容量,实例如下:

实例演示

public class GetCapacity {
  public static void main(String[] args) {
    // 调用StringBuilder的无参构造方法,生成一个str对象
    StringBuilder str = new StringBuilder();
    System.out.println("str的初始容量为:" + str.capacity());
    // 循环执行连接操作
    for (int i = 0; i 16; i ++) {
      str.append(i);
    }
    System.out.println("连接操作后,str的容量为" + str.capacity());
  }
}
 可查看在线运行效果

运行结果:

str的初始容量为:16
连接操作后,str的容量为34
代码块12
3.2.3 字符串替换

可以使用 StringBuilder replace(int start, int end, String str) 方法,来用指定字符串替换从索引位置 start 开始到 end 索引位置结束(不包含 end)的子串。实例如下:

实例演示

public class StringReplace {
  public static void main(String[] args) {
    // 初始化一个内容为 Hello 的字符串生成器
    StringBuilder str = new StringBuilder("Hello World!");
    // 调用字符串替换方法,将 World 替换为 Java
    str.replace(6, 11, "Java");
    // 打印替换后的字符串
    System.out.println(str);
  }
}
12345678910

可查看在线运行效果

运行结果:

Hello Java!
代码块1

也可使用 StringBuilder delete(int start, int end) 方法,先来删除索引位置 start 开始到 end 索引位置(不包含 end)的子串,再使用 StringBuilder insert(int offset, String str) 方法,将字符串插入到序列的 offset 索引位置。同样可以实现字符串的替换,例如:

StringBuilder str = new StringBuilder("Hello World!");
str.delete(6, 11);
str.insert(6, "Java");
代码块123
3.2.4 字符串截取

可以使用 StringBuilder substring(int start) 方法来进行字符串截取,例如,我们想截取字符串的后三个字符,实例如下:

实例演示

public class StringSub {
  public static void main(String[] args) {
    StringBuilder str = new StringBuilder("你好,欢迎来到夜猫编程");
    String substring = str.substring(7);
    System.out.println("str截取后子串为:" + substring);
  }
}
1234567

可查看在线运行效果

运行结果:

str截取后子串为:夜猫编程
代码块1

如果我们想截取示例中的” 欢迎 “二字,可以使用重载方法 StringBuilder substring(int start, int end) 进行截取:

String substring = str.substring(3, 5);
代码块1
3.2.5 字符串反转

可以使用 StringBuildr reverse() 方法,对字符串进行反转操作,例如:

实例演示

public class StringReverse {
  public static void main(String[] args) {
    StringBuilder str = new StringBuilder("Hello Java");
    System.out.println("str经过反转操作后为:" + str.reverse());
  }
}
123456

可查看在线运行效果

运行结果:

str经过反转操作后为:avaJ olleH
代码块1
Java Scanner 类

一直以来,我们都使用System.out.println()方法向屏幕打印内容,那么如何接收输入的内容呢?本小节所学习的Scanner类就可以实现对输入内容的接收。在本小节,我们将学习Scanner类的定义,如何使用Scanner类以及其常用方法,在学完这些基础知识后,我们会在最后学习一个比较有趣的实例程序。

1. 定义

Scanner是一个简单的文本扫描器,可以解析基础数据类型和字符串。

它位于java.util包下,因此如果要使用此类,必须使用import语句导入:

import java.util.Scanner;
代码块1
2. Scanner 对象创建

想要使用Scanner类就要了解如何创建对象,我们可以使用如下代码创建一个扫描器对象:

Scanner scanner = new Scanner(System.in);
代码块1

构造方法的参数System.in表示允许用户从系统中读取内容。本小节,我们的示例代码中都将使用这个构造方法。

Tips:System.in是一个InputStream类型,Scanner类还有很多接收其他类型的构造方法。这里不详细介绍。

3. 常用方法3.1 next()及其同伴方法

想要获取用户的输入,只有对象是不行的,还要配合它的实例方法。此时配合Scanner类中的next()方法及其同伴方法可以获取指定类型的输入。

3.1.1 next() 方法

next()方法的返回值是字符串类型,可以使用此方法,将用户输入的内容扫描为字符串。我们来看一个示例,获取并打印用户输入的内容:

import java.util.Scanner;
public class ScannerDemo1 {
  public static void main(String[] args) {
    // 创建扫描器对象
    Scanner scanner = new Scanner(System.in);
    System.out.println("请输入一段内容,输入回车结束:");
    // 可以将用户输入的内容扫描为字符串
    String str = scanner.next();
    // 打印输出
    System.out.println("您输入的内容为:" + str);
    // 关闭扫描器
    scanner.close();
  }
}
代码块123456789101112131415

在代码中我们注意到,在代码块的最后调用了close()方法,这个方法用于关闭当前扫描器,就和电脑的开关机一样,使用电脑前要开机,而当用不到的时候最好关掉。

编译执行代码,屏幕将会提示:

请输入一段内容,输入回车结束:
代码块1

接下来我们按照提示输入内容,然后输入回车结束输入:

3.1.2 同伴方法

那什么是同伴方法呢?这里的同伴方法指的是Scanner类中以next单词开头的方法。我们举例来看几个同伴方法及其作用:

nextLine() :返回输入回车之前的所有字符;

nextInt() :将输入内容扫描为int类型;

nextFloat() :将输入内容扫描为float类型。

这里的nextLine() 方法也可以获取字符串。我们来看一下它和next()方法的差异:

next()方法只有扫描到有效字符后才会结束输入;而nextLine()方法可以直接使用回车结束输入。

另外,next()方法会自动去掉空白(例如回车、空格等),也不能得到带有空格的字符串;nextLine()方法可以得到空白和带有空格的字符串。

我们再来看一个示例,获取用户输入的姓名、年龄和身高,并且打印输出:

import java.util.Scanner;
public class ScannerDemo2 {
  public static void main(String[] args) {
    // 创建扫描器对象
    Scanner scanner = new Scanner(System.in);
    System.out.println("请输入您的姓名:");
    // 将第一行输入扫描为字符串
    String name = scanner.nextLine();
    System.out.println("请输入您的年龄:");
    // 将第二行输入扫描为int类型
    int age = scanner.nextInt();
    System.out.println("请输入您的身高:");
    // 将第三行输入扫描为float类型
    float height = scanner.nextFloat();
    // 打印扫描器所扫描的值
    System.out.println("您的姓名为:" + name);
    System.out.println("您的年龄为:" + age);
    System.out.println("您的身高为:" + height);
    // 关闭扫描器
    scanner.close();
  }
}
代码块1234567891011121314151617181920212223242526

编译执行代码,按照提示输入对应内容,直到程序完整运行:

请输入您的姓名:
三井 寿
请输入您的年龄:
19
请输入您的身高:
183
您的姓名为:三井 寿
您的年龄为:19
您的身高为:183
代码块123456789

Tips:上面代码中,如果使用next()方法代替nextLine()方法来获取姓名字符串,是无法得到我们输入的“三井 寿”这个字符串的,这是因为next()方法不能获取带有空格的字符串。

要特别注意的是:Scanner 类读到的内容,只与输入顺序有关,和终端上显示的顺序无关,因此类似于下面的这种输入,是读不到空格的,执行代码的流程如下:

3.2 hasNext()及其同伴方法

hasNext()方法的返回值是一个布尔类型,如果输入中包含数据的输入,则返回true。否则返回false。通常用来做输入内容的验证。

它的同伴方法是以hasNext单词开头的方法,诸如hasNextLine()、hasNextInt()等方法。例如,上面的代码中,我们可以对应加入hasNext同伴方法结合条件判断语句,来提升代码的稳定性:

int age;
if (scanner.hasNextInt()) {
  age = scanner.nextInt();
} else {
  System.out.println("不是int类型");
}
float height;
if (scanner.hasNextFloat()) {
  height = scanner.nextFloat();
} else {
  System.out.println("不是float类型");
}
代码块12345678910111213
4. 实例

前面我们已经对Scanner类的基本用法有了一定的了解,下面我们来实现一个示例程序,这个程序用于估算一个人的体脂率,这里事先给出体脂率估算公式:

参数a = 腰围(cm)×0.74  
参数b = 体重(kg)× 0.082 + 44.74
脂肪重量(kg)= a - b 
体脂率 =(脂肪重量 ÷ 体重)× 100%。 
代码块1234

从公式中我们可以看出,想要得到最终的体脂率,参数a(腰围)和参数 b(体重)是需要用户手动输入的,公式部分只需要使用算数运算符实现即可。下面是程序代码:

import java.util.Scanner;
public class GetBodyFat {
  public static void main(String[] args) {
    // 初始化腰围
    float waistline = 0f;
    // 初始化体重
    float weight = 0f;
    // 声明浮点型参数a,b,bodyFatWeight(脂肪重量)
    float a, b, bodyFatWeight;
    Scanner scanner = new Scanner(System.in);
    System.out.println("请输入您的腰围(cm):");
    if (scanner.hasNextFloat()) {
      waistline = scanner.nextFloat();
    }
    System.out.println("请输入您的体重(kg):");
    if (scanner.hasNextFloat()) {
      weight = scanner.nextFloat();
    }
    // 计算参数a 公式:参数a = 腰围(cm)× 0.74
    a = waistline * 0.74f;
    // 计算参数b 公式:参数b = 体重(kg)× 0.082 + 44.74
    b = weight * 0.082f + 44.74f;
    // 计算脂肪重量
    bodyFatWeight = a - b;
    // 计算体脂率 =(脂肪重量 ÷ 体重)×100%。
    float result = bodyFatWeight / weight * 100;
    System.out.println("您的体脂率为" + result + "%");
  }
}
代码块123456789101112131415161718192021222324252627282930

编译运行代码,按照提示输入,将估算出你的体脂含量:

请输入您的腰围(cm):
70
请输入您的体重(kg):
50
您的体脂率为5.919998%
代码块12345

执行代码的流程如下:

Java 异常处理

Java 的异常处理是 Java 语言的一大重要特性,也是提高代码健壮性的最强大方法之一。当我们编写了错误的代码时,编译器在编译期间可能会抛出异常,有时候即使编译正常,在运行代码的时候也可能会抛出异常。本小节我们将介绍什么是异常、Java 中异常类的架构、如何进行异常处理、如何自定义异常、什么是异常链、如何使用异常链等内容。

1. 什么是异常

异常就是程序上的错误,我们在编写程序的时候经常会产生错误,这些错误划分为编译期间的错误和运行期间的错误。

下面我们来看几个常见的异常案例。

如果语句漏写分号,程序在编译期间就会抛出异常,实例如下:

public class Hello {
  public static void main(String[] args) {
    System.out.println("Hello World!")
  }
}
代码块12345

运行结果:

$$ javac Hello.java
Hello.java:3: 错误: 需要';'
    System.out.println("Hello World!")
                     ^
1 个错误
代码块12345

运行过程:

由于代码的第 3 行语句漏写了分号,Java 编译器给出了明确的提示。

static 关键字写成了 statci,实例如下:

Hello.java:2: 错误: 需要 标识符 
  public statci void main(String[] args) {
         ^
1 个错误
代码块1234

当数组下标越界,程序在编译阶段不会发生错误,但在运行时会抛出异常。实例如下:

public class ArrayOutOfIndex {
  public static void main(String[] args) {
    int[] arr = {1, 2, 3};
    System.out.println(arr[3]);
  }
}
代码块123456

运行结果:

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
 at ArrayOutOfIndex.main(ArrayOutOfIndex.java:4)
代码块12

运行过程:

2. Java 异常类架构

在 Java 中,通过 Throwable 及其子类来描述各种不同类型的异常。如下是 Java 异常类的架构图(不是全部,只展示部分类):

2.1 Throwable 类

Throwable 位于 java.lang 包下,它是 Java 语言中所有错误(Error)和异常(Exception)的父类。

Throwable 包含了其线程创建时线程执行堆栈的快照,它提供了 printStackTrace() 等接口用于获取堆栈跟踪数据等信息。

主要方法:

fillInStackTrace: 用当前的调用栈层次填充 Throwable 对象栈层次,添加到栈层次任何先前信息中;

getMessage:返回关于发生的异常的详细信息。这个消息在 Throwable 类的构造函数中初始化了;

getCause:返回一个 Throwable 对象代表异常原因;

getStackTrace:返回一个包含堆栈层次的数组。下标为 0 的元素代表栈顶,最后一个元素代表方法调用堆栈的栈底;

printStackTrace:打印 toString() 结果和栈层次到 System.err,即错误输出流。

2.2 Error 类

Error 是 Throwable 的一个直接子类,它可以指示合理的应用程序不应该尝试捕获的严重问题。这些错误在应用程序的控制和处理能力之外,编译器不会检查 Error,对于设计合理的应用程序来说,即使发生了错误,本质上也无法通过异常处理来解决其所引起的异常状况。

常见 Error:

AssertionError:断言错误;

VirtualMachineError:虚拟机错误;

UnsupportedClassVersionError:Java 类版本错误;

OutOfMemoryError :内存溢出错误。

2.3 Exception 类

Exception 是 Throwable 的一个直接子类。它指示合理的应用程序可能希望捕获的条件。

Exception 又包括 Unchecked Exception(非检查异常)和 Checked Exception(检查异常)两大类别。

2.3.1 Unchecked Exception (非检查异常)

Unchecked Exception 是编译器不要求强制处理的异常,包含 RuntimeException 以及它的相关子类。我们编写代码时即使不去处理此类异常,程序还是会编译通过。

常见非检查异常:

NullPointerException:空指针异常;

ArithmeticException:算数异常;

ArrayIndexOutOfBoundsException:数组下标越界异常;

ClassCastException:类型转换异常。

2.3.2 Checked Exception(检查异常)

Checked Exception 是编译器要求必须处理的异常,除了 RuntimeException 以及它的子类,都是 Checked Exception 异常。我们在程序编写时就必须处理此类异常,否则程序无法编译通过。

常见检查异常:

IOException:IO 异常

SQLException:SQL 异常

3. 如何进行异常处理

在 Java 语言中,异常处理机制可以分为两部分:

抛出异常:当一个方法发生错误时,会创建一个异常对象,并交给运行时系统处理;

捕获异常:在方法抛出异常之后,运行时系统将转为寻找合适的异常处理器。

Java 通过 5 个关键字来实现异常处理,分别是:throw、throws、try、catch、finally。

异常总是先抛出,后捕获的。下面我们将围绕着 5 个关键字来详细讲解如何抛出异常以及如何捕获异常。

4. 抛出异常4.1 实例

我们先来看一个除零异常的实例代码:

public class ExceptionDemo1 {
  // 打印 a / b 的结果
  public static void divide(int a, int b) {
    System.out.println(a / b);
  }
  public static void main(String[] args) {
    // 调用 divide() 方法
    divide(2, 0);
  }
}
代码块1234567891011

运行结果:

Exception in thread "main" java.lang.ArithmeticException: / by zero
 at ExceptionDemo1.divide(ExceptionDemo1.java:4)
 at ExceptionDemo1.main(ExceptionDemo1.java:9)
代码块123

运行过程:

我们知道 0 是不能用作除数的,由于 divide() 方法中除数 b 为 0,所以代码将停止执行并显示了相关的异常信息,此信息为堆栈跟踪,上面的运行结果告诉我们:main 线程发生了类型为 ArithmeticException 的异常,显示消息为 by zero,并且提示了可能发生异常的方法和行号。

4.2 throw

上面的实例中,程序在运行时引发了错误,那么如何来显示抛出(创建)异常呢?

我们可以使用 throw 关键字来抛出异常,throw 关键字后面跟异常对象,改写上面的实例代码:

public class ExceptionDemo2 {
  // 打印 a / b 的结果
  public static void divide(int a, int b) {
    if (b == 0) {
      // 抛出异常
      throw new ArithmeticException("除数不能为零");
    }
    System.out.println(a / b);
  }
  public static void main(String[] args) {
    // 调用 divide() 方法
    divide(2, 0);
  }
}
代码块123456789101112131415

运行结果:

Exception in thread "main" java.lang.ArithmeticException: 除数不能为零
 at ExceptionDemo2.divide(ExceptionDemo2.java:5)
 at ExceptionDemo2.main(ExceptionDemo2.java:12)
代码块123

运行过程:

代码在运行时同样引发了错误,但显示消息为 “除数不能为零”。我们看到 divide() 方法中加入了条件判断,如果调用者将参数 b 设置为 0 时,会使用 throw 关键字来抛出异常,throw 后面跟了一个使用 new 关键字实例化的算数异常对象,并且将消息字符串作为参数传递给了算数异常的构造函数。

我们可以使用 throw 关键字抛出任何类型的 Throwable 对象,它会中断方法,throw 语句之后的所有内容都不会执行。除非已经处理抛出的异常。异常对象不是从方法中返回的,而是从方法中抛出的。

4.3 throws

可以通过 throws 关键字声明方法要抛出何种类型的异常。如果一个方法可能会出现异常,但是没有能力处理这种异常,可以在方法声明处使用 throws 关键字来声明要抛出的异常。例如,汽车在运行时可能会出现故障,汽车本身没办法处理这个故障,那就让开车的人来处理。

throws 用在方法定义时声明该方法要抛出的异常类型,如下是伪代码:

public void demoMethod() throws Exception1, Exception2, ... ExceptionN {
  // 可能产生异常的代码
}
代码块123

throws 后面跟的异常类型列表可以有一个也可以有多个,多个则以 , 分割。当方法产生异常列表中的异常时,将把异常抛向方法的调用方,由调用方处理。

throws 有如下使用规则:

如果方法中全部是非检查异常(即 Error、RuntimeException 以及的子类),那么可以不使用 throws 关键字来声明要抛出的异常,编译器能够通过编译,但在运行时会被系统抛出;

如果方法中可能出现检查异常,就必须使用 throws 声明将其抛出或使用 try catch 捕获异常,否则将导致编译错误;

当一个方法抛出了异常,那么该方法的调用者必须处理或者重新抛出该异常;

当子类重写父类抛出异常的方法时,声明的异常必须是父类所声明异常的同类或子类。

5. 捕获异常

使用 try 和 catch 关键字可以捕获异常。try catch 代码块放在异常可能发生的地方。它的语法如下:

try {
  // 可能会发生异常的代码块
} catch (Exception e1) {
  // 捕获并处理try抛出的异常类型Exception
} catch (Exception2 e2) {
  // 捕获并处理try抛出的异常类型Exception2
} finally {
  // 无论是否发生异常,都将执行的代码块
}
代码块123456789

我们来看一下上面语法中的 3 种语句块:

try 语句块:用于监听异常,当发生异常时,异常就会被抛出;

catch 语句块:catch 语句包含要捕获的异常类型的声明,当 try 语句块发生异常时,catch 语句块就会被检查。当 catch 块尝试捕获异常时,是按照 catch 块的声明顺序从上往下寻找的,一旦匹配,就不会再向下执行。因此,如果同一个 try 块下的多个 catch 异常类型有父子关系,应该将子类异常放在前面,父类异常放在后面;

finally 语句块:无论是否发生异常,都会执行 finally 语句块。finally 常用于这样的场景:由于 finally 语句块总是会被执行,所以那些在 try 代码块中打开的,并且必须回收的物理资源(如数据库连接、网络连接和文件),一般会放在 finally 语句块中释放资源。

try 语句块后可以接零个或多个 catch 语句块,如果没有 catch 块,则必须跟一个 finally 语句块。简单来说,try 不允许单独使用,必须和 catch 或 finally 组合使用,catch 和 finally 也不能单独使用。

实例如下:

public class ExceptionDemo3 {
  // 打印 a / b 的结果
  public static void divide(int a, int b) {
    System.out.println(a / b);
  }
  public static void main(String[] args) {
    try {
      // try 语句块
      // 调用 divide() 方法
      divide(2, 0);
    } catch (ArithmeticException e) {
      // catch 语句块
      System.out.println("catch: 发生了算数异常:" + e);
    } finally {
      // finally 语句块
      System.out.println("finally: 无论是否发生异常,都会执行");
    }
  }
}
代码块1234567891011121314151617181920

运行结果:

catch: 发生了算数异常:java.lang.ArithmeticException: / by zero
finally: 无论是否发生异常,都会执行
代码块12

运行过程:

divide() 方法中除数 b 为 0,会发生除零异常,我们在方法调用处使用了 try 语句块对异常进行捕获;如果捕获到了异常, catch 语句块会对 ArithmeticException 类型的异常进行处理,此处打印了一行自定义的提示语句;最后的 finally 语句块,无论发生异常与否,总会执行。

Java 7 以后,catch 多种异常时,也可以像下面这样简化代码:

try {
  // 可能会发生异常的代码块
} catch (Exception | Exception2 e) {
  // 捕获并处理try抛出的异常类型
} finally {
  // 无论是否发生异常,都将执行的代码块
}
代码块1234567
6. 自定义异常

自定义异常,就是定义一个类,去继承 Throwable 类或者它的子类。

Java 内置了丰富的异常类,通常使用这些内置异常类,就可以描述我们在编码时出现的大部分异常情况。一旦内置异常无法满足我们的业务要求,就可以通过自定义异常描述特定业务产生的异常类型。

实例:

public class ExceptionDemo4 {
  static class MyCustomException extends RuntimeException {
    /**
     * 无参构造方法
     */
    public MyCustomException() {
      super("我的自定义异常");
    }
  }
  public static void main(String[] args) {
   // 直接抛出异常
    throw new MyCustomException();
  }
}
代码块12345678910111213141516

运行结果:

Exception in thread "main" ExceptionDemo4$$MyCustomException: 我的自定义异常
 at ExceptionDemo4.main(ExceptionDemo4.java:13)
代码块12

运行过程:

在代码中写了一个自定义异常 MyCustomException,继承自 RuntimeException,它是一个静态内部类,这样在主方法中就可以直接抛出这个异常类了。当然,也可以使用 catch 来捕获此类型异常。

7. 异常链

异常链是以一个异常对象为参数构造新的异常对象,新的异常对象将包含先前异常的信息。简单来说,就是将异常信息从底层传递给上层,逐层抛出,我们来看一个实例:

public class ExceptionDemo5 {
  /**
   * 第一个自定义的静态内部异常类
   */
  static class FirstCustomException extends Exception {
    // 无参构造方法
    public FirstCustomException() {
      super("第一个异常");
    }
  }
  /**
   * 第二个自定义的静态内部异常类
   */
  static class SecondCustomException extends Exception {
    public SecondCustomException() {
      super("第二个异常");
    }
  }
  /**
   * 第三个自定义的静态内部异常类
   */
  static class ThirdCustomException extends Exception {
    public ThirdCustomException() {
      super("第三个异常");
    }
  }
  /**
   * 测试异常链静态方法1,直接抛出第一个自定义的静态内部异常类
   * @throws FirstCustomException
   */
  public static void f1() throws FirstCustomException {
    throw new FirstCustomException();
  }
  /**
   * 测试异常链静态方法2,调用f1()方法,并抛出第二个自定义的静态内部异常类
   * @throws SecondCustomException
   */
  public static void f2() throws SecondCustomException {
    try {
      f1();
    } catch (FirstCustomException e) {
      throw new SecondCustomException();
    }
  }
  /**
   * 测试异常链静态方法3,调用f2()方法, 并抛出第三个自定义的静态内部异常类
   * @throws ThirdCustomException
   */
  public static void f3() throws ThirdCustomException {
    try {
      f2();
    } catch (SecondCustomException e) {
      throw new ThirdCustomException();
    }
  }
  public static void main(String[] args) throws ThirdCustomException {
    // 调用静态方法f3()
    f3();
  }
}
代码块12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970

运行结果:

Exception in thread "main" ExceptionDemo5$$ThirdCustomException: 第三个异常
 at ExceptionDemo5.f3(ExceptionDemo5.java:46)
 at ExceptionDemo5.main(ExceptionDemo5.java:51)
代码块123

运行过程:

通过运行结果,我们只获取到了静态方法 f3() 所抛出的异常堆栈信息,前面代码所抛出的异常并没有被显示。

我们改写上面的代码,让异常信息以链条的方式 “连接” 起来。可以通过改写自定义异常的构造方法,来获取到之前异常的信息。实例如下:

/**
 * @author colorful@TaleLin
 */
public class ExceptionDemo6 {
  /**
   * 第一个自定义的静态内部异常类
   */
  static class FirstCustomException extends Exception {
    // 无参构造方法
    public FirstCustomException() {
      super("第一个异常");
    }
  }
  /**
   * 第二个自定义的静态内部异常类
   */
  static class SecondCustomException extends Exception {
    /**
     * 通过构造方法获取之前异常的信息
     * @param cause 捕获到的异常对象
     */
    public SecondCustomException(Throwable cause) {
      super("第二个异常", cause);
    }
  }
  /**
   * 第三个自定义的静态内部异常类
   */
  static class ThirdCustomException extends Exception {
    /**
     * 通过构造方法获取之前异常的信息
     * @param ca(户籍所在地怎么填写?户籍所在地是指我国居民户口簿登记所在地,一般是指出生时其父母户口登记地方。按照户口登记管理条例,公民填写户籍所在地,应该填写到户籍管理机关所在地,即城市户口的应该填**省**市(县)**区;农村户口的应该填**省**县**乡。一般在填写户籍所在地时,只填写到县就可以了。)use 捕获到的异常对象
     */
    public ThirdCustomException(Throwable cause) {
      super("第三个异常", cause);
    }
  }
  /**
   * 测试异常链静态方法1,直接抛出第一个自定义的静态内部异常类
   * @throws FirstCustomException
   */
  public static void f1() throws FirstCustomException {
    throw new FirstCustomException();
  }
  /**
   * 测试异常链静态方法2,调用f1()方法,并抛出第二个自定义的静态内部异常类
   * @throws SecondCustomException
   */
  public static void f2() throws SecondCustomException {
    try {
      f1();
    } catch (FirstCustomException e) {
      throw new SecondCustomException(e);
    }
  }
  /**
   * 测试异常链静态方法3,调用f2()方法, 并抛出第三个自定义的静态内部异常类
   * @throws ThirdCustomException
   */
  public static void f3() throws ThirdCustomException {
    try {
      f2();
    } catch (SecondCustomException e) {
      throw new ThirdCustomException(e);
    }
  }
  public static void main(String[] args) throws ThirdCustomException {
    // 调用静态方法f3()
    f3();
  }
}
代码块12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182

运行结果:

Exception in thread "main" ExceptionDemo6$$ThirdCustomException: 第三个异常
 at ExceptionDemo6.f3(ExceptionDemo6.java:74)
 at ExceptionDemo6.main(ExceptionDemo6.java:80)
Caused by: ExceptionDemo6$$SecondCustomException: 第二个异常
 at ExceptionDemo6.f2(ExceptionDemo6.java:62)
 at ExceptionDemo6.f3(ExceptionDemo6.java:72)
 ... 1 more
Caused by: ExceptionDemo6$$FirstCustomException: 第一个异常
 at ExceptionDemo6.f1(ExceptionDemo6.java:51)
 at ExceptionDemo6.f2(ExceptionDemo6.java:60)
 ... 2 more
代码块1234567891011

运行过程:

通过运行结果,我们看到,异常发生的整个过程都打印到了屏幕上,这就是一个异常链。

1、通过本小节的学习,我们知道了异常就是程序上的错误,良好的异常处理可以提高代码的健壮性。Java 语言中所有错误(Error)和异常(Exception)的父类都是 Throwable。Error 和 Exception 是 Throwable 的直接子类,我们通常说的异常处理实际上就是处理 Exception 及其子类,异常又分为检查型异常和非检查型异常。通过抛出异常和捕获异常来实现异常处理。我们亦可以通过继承 Throwable 类或者它的子类来自定义异常类。通过构造方法获取之前异常的信息可以实现异常链。

2、本小节我们学习了 Java 的 Scanner类,它是位于java.util包下的一个工具类,我们知道了它是一个简单的文本扫描器,可以解析基础数据类型和字符串。我们也学会了如何使用Scanner类来获取用户的输入,next()方法和nextLine()方法都可以扫描用户输入的字符串,要注意这两个方法的区别。我们也在最后给出了一个计算体脂率的示例代码,学习了Scanner类,你就可以实现比较有意思的一些小程序了。如果你想了解更多有关Scanner类的接口,也可翻阅官方文档。

3、本小节我们介绍了 Java 的 StringBuilder 类,它具有可变性,对于频繁操作字符串的场景,使用它来代替 String 类可以提高程序的执行效率;也知道了 StringBuffer 是 StringBuilder 的线程安全版本,官方更推荐使用 StringBuilder;最后我们介绍了 StringBuilder 的常用构造方法和成员方法,如果你想了解更多关于 StringBuilder 的接口,可以翻阅官方文档进行学习。

4、本小节我们介绍了 Java String类的常用方法:

使用length()方法可以获取字符串长度;

使用charAt()、indexOf()以及lastIndexOf()方法可以对字符串进行查找;

substring()方法可以对字符串的进行截取,split()、getBytes()方法可以将字符串切割为数组;

toLowerCase()和toUpperCase()方法分别用于大小写转换,使用equals()方法对字符串进行比较,这里要注意,对字符串内容进行比较时,永远都不要使用==运算符。

这些方法大多有重载方法,实际工作中,要根据合适的场景选用对应的重载方法。

当然,本小节还有很多未介绍到的方法,使用到可以翻阅官网文档来进行学习。

本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。