基本类型包装类

包装类介绍

包装类层次结构如下:
"包装类"

其中能够表示数字的基本类型包装类继承自Number类:

  • byte -> Byte
  • boolean -> Boolean
  • short -> Short
  • char -> Character
  • int -> Integer
  • long -> Long
  • float -> Float
  • double -> Double

可以直接使用,以Integer类为例:

1
2
3
4
5
6
7
package org.ep;

public class Main {
public static void main(String[] args) {
Integer i = new Integer(10);
}
}

包装类实际上就是将我们的基本数据类型,封装成一个类,以Integer为例:

1
2
3
4
5
private final int value;  //类中实际上就靠这个变量在存储包装的值

public Integer(int value) {
this.value = value;
}

包装类型支持自动装箱,我们可以直接将一个对应的基本类型值作为对应包装类型引用变量的值:

1
2
3
4
5
6
7
package org.ep;

public class Main {
public static void main(String[] args) {
Integer i = 10;
}
}

这种写法等价于:

1
2
3
4
5
6
7
package org.ep;

public class Main {
public static void main(String[] args) {
Integer i = Integer.valueOf(10);
}
}

这里本质上就是被自动包装成了一个Integer类型的对象,只是语法上为了简单,就支持像这样编写。同时包装类也支持拆箱:

1
2
3
4
5
6
7
8
package org.ep;

public class Main {
public static void main(String[] args) {
Integer i = 10;
int a = i;
}
}

这种写法等价于:

1
2
3
4
5
6
7
8
package org.ep;

public class Main {
public static void main(String[] args) {
Integer i = 10;
int a = i.intValue();
}
}

得益于包装类型的自动装箱和拆箱机制,我们可以让包装类型轻松地参与到基本类型的运算中:

1
2
3
4
5
6
7
8
9
package org.ep;

public class Main {
public static void main(String[] args) {
Integer a = 10, b = 20;
int c = a + b;
System.out.println(c);
}
}
1
30

但因为包装类是一个类,不是基本类型,所以说两个不同的对象,那么是不相等的:

1
2
3
4
5
6
7
8
9
package org.ep;

public class Main {
public static void main(String[] args) {
Integer a = new Integer(10);
Integer b = new Integer(10);
System.out.println(a == b);
}
}
1
false

但通过自动装箱转换的Integer对象比较呢:

1
2
3
4
5
6
7
8
9
package org.ep;

public class Main {
public static void main(String[] args) {
Integer a = 10;
Integer b = 10;
System.out.println(a == b);
}
}
1
true

通过自动装箱转换的Integer对象如果值相同,得到的会是同一个对象。这是因为:

1
2
3
4
5
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high) //这里会有一个IntegerCache,如果在范围内,那么会直接返回已经提前创建好的对象
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}

IntegerCache会默认缓存-128~127之间的所有值,将这些值提前做成包装类放在数组中存放。但是如果超出这个缓存范围的话,就会得到不同的对象了:

1
2
3
4
5
6
7
8
9
package org.ep;

public class Main {
public static void main(String[] args) {
Integer a = 128;
Integer b = 128;
System.out.println(a == b);
}
}
1
false

同样的,LongShortByte类型的包装类也有类似的机制,感兴趣的小伙伴可以自己点进去看看。

包装类中同时还提供了其他不同的方法:

字符串转换整数:

1
2
3
4
5
6
7
8
package org.ep;

public class Main {
public static void main(String[] args) {
Integer a = new Integer("123");
System.out.println(a);
}
}
1
123

另一种写法:

1
2
3
4
5
6
7
8
package org.ep;

public class Main {
public static void main(String[] args) {
Integer a = Integer.valueOf("123");
System.out.println(a);
}
}
1
123

可以对十六进制和八进制的字符串进行解码,得到对应的int值:

1
2
3
4
5
6
7
8
package org.ep;

public class Main {
public static void main(String[] args) {
Integer i = Integer.decode("0xA6");
System.out.println(i);
}
}
1
166

也可以将十进制的整数转换为其他进制的字符串:

1
2
3
4
5
6
7
package org.ep;

public class Main {
public static void main(String[] args) {
System.out.println(Integer.toHexString(166));
}
}
1
a6

特殊包装类

除了几种基本类型包装之外还有几个比较特殊的包装类型。

BigInteger

其中第一个是用于计算超大数字的BigInteger,因为即使是最大的long类型,也只能表示64bit的数据,无法表示一个非常大的数,但是BigInteger没有这些限制,我们可以让他等于一个非常大的数字:

1
2
3
4
5
6
7
8
9
10
package org.ep;

import java.math.BigInteger;

public class Main {
public static void main(String[] args) {
BigInteger i = BigInteger.valueOf(Long.MAX_VALUE); //表示Long的最大值
System.out.println(i);
}
}
1
9223372036854775807

可以调用类中的方法进行运算:

1
2
3
4
5
6
7
8
9
10
11
package org.ep;

import java.math.BigInteger;

public class Main {
public static void main(String[] args) {
BigInteger i = BigInteger.valueOf(Long.MAX_VALUE); //表示Long的最大值,轻轻松松
i = i.multiply(BigInteger.valueOf(Long.MAX_VALUE));
System.out.println(i);
}
}
1
85070591730234615847396907784232501249

对于非常大的整数计算,就可以使用BigInteger来实现。

BigDecimal

浮点类型精度有限,当我们需要精确计算时,可以使用BigDecimal实现小数的精确计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.ep;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;

public class Main {
public static void main(String[] args) {
BigDecimal i = BigDecimal.valueOf(10);
i = i.divide(BigDecimal.valueOf(3), 100, RoundingMode.CEILING);
//计算10/3的结果,精确到小数点后100位
//RoundingMode是舍入模式,就是精确到最后一位时,该怎么处理,这里CEILING表示向上取整
System.out.println(i);
}
}
1
3.3333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333334

StringBuilder

在之前已经了解,字符串支持+进行操作:

1
2
3
4
5
6
7
8
package org.ep;

public class Main {
public static void main(String[] args) {
String str = "Hello" + " World";
System.out.println(str);
}
}
1
Hello World

但拼接字符串效率很低,所以编译器在处理+进行的字符串拼接的时候会有优化,比如:

1
2
3
4
5
6
7
8
9
10
11
12
package org.ep;

public class Main {
public static void main(String[] args) {
String str1 = "R";
String str2 = "a";
String str3 = "i";
String str4 = "n";
String str = str1 + str2 + str3 + str4;
System.out.println(str);
}
}
1
Rain

这种写法实际上会被优化为下面的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
package org.ep;

public class Main {
public static void main(String[] args) {
String str1 = "R";
String str2 = "a";
String str3 = "i";
String str4 = "n";
StringBuilder sb = new StringBuilder();
sb.append(str1).append(str2).append(str3).append(str4);
System.out.println(sb.toString());
}
}

这里创建了一个StringBuilder的类型,这个类型专门用于构造字符串,可以使用它对字符串进行拼接、裁剪等操作,弥补了字符串不能修改的不足。

实现裁剪操作:

1
2
3
4
5
6
7
8
9
package org.ep;

public class Main {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("AAABBB");
sb.delete(2, 4);
System.out.println(sb);
}
}
1
AABB

正则表达式

如果想要实现这样的功能:对于给定的字符串进行判断,如果字符串符合我们的规则,那么就返回真,否则返回假。比如现在我们想要判断字符串是不是邮箱的格式:

1
2
3
4
5
6
7
8
package org.ep;

public class Main {
public static void main(String[] args) {
String str = "youxiang@163.com";
//假设邮箱格式为数字/字母@数字/字母.com
}
}

就可以使用正则表达式来解决这种问题。

正则表达式(regular expression)描述了一种字符串匹配的模式(pattern),可以用来检查一个串是否含有某种子串、将匹配的子串替换或者从某个串中取出符合某个条件的子串等。

例如:

1
2
3
4
5
6
7
8
package org.ep;

public class Main {
public static void main(String[] args) {
String str = "oooo"; //matches方法用于对给定正则表达式进行匹配,匹配成功返回true,否则返回false
System.out.println(str.matches("o+")); //+表示对前面这个字符匹配一次或多次,这里字符串是oooo,正好可以匹配
}
}
1
true

用于规定给定组件必须要出现多少次才能满足匹配的,称为限定符,限定符表如下:

字符 描述
* 匹配前面的子表达式零次或多次。例如,zo* 能匹配 “z” 以及 “zoo”。***** 等价于 {0,}。
+ 匹配前面的子表达式一次或多次。例如,zo+ 能匹配 “zo” 以及 “zoo”,但不能匹配 “z”。+ 等价于 {1,}。
? 匹配前面的子表达式零次或一次。例如,do(es)? 可以匹配 “do” 、 “does”、 “doxy” 中的 “do” 。? 等价于 {0,1}。
{n} n 是一个非负整数。匹配确定的 n 次。例如,o{2} 不能匹配 “Bob” 中的 o,但是能匹配 “food” 中的两个 o。
{n,} n 是一个非负整数。至少匹配n 次。例如,o{2,} 不能匹配 “Bob” 中的 o,但能匹配 “foooood” 中的所有 o。o{1,} 等价于 o+。o{0,} 则等价于 o*。
{n,m} m 和 n 均为非负整数,其中 n <= m。最少匹配 n 次且最多匹配 m 次。例如,o{1,3} 将匹配 “fooooood” 中的前三个 o。o{0,1} 等价于 o?。请注意在逗号和两个数之间不能有空格。
如果想要表示一个范围内的字符,可以使用方括号[]
1
2
3
4
5
6
7
8
package org.ep;

public class Main {
public static void main(String[] args) {
String str = "abcabccaa";
System.out.println(str.matches("[abc]+")); //表示"abc"这几个字符可以匹配一次或多次
}
}
1
true

对于普通字符来说,可以用下面的方式实现多种字符匹配:

字符 描述
[ABC] 匹配 [] 中的所有字符,例如 [aeiou] 匹配字符串 “google runoob taobao” 中所有的 e o u a 字母。
[^ABC] 匹配除了 [] 中字符的所有字符,例如 [^aeiou] 匹配字符串 “google runoob taobao” 中除了 e o u a 字母的所有字母。
[A-Z] [A-Z] 表示一个区间,匹配所有大写字母,[a-z] 表示所有小写字母。
. 匹配除换行符(\n、\r)之外的任何单个字符,相等于 [^\n\r]
[\s\S] 匹配所有。\s 是匹配所有空白符,包括换行,\S 非空白符,不包括换行。
\w 匹配字母、数字、下划线。等价于 [A-Za-z0-9_]

正则表达式其他内容:https://www.runoob.com/regexp/regexp-syntax.html

内部类

内部类顾名思义,就是创建在内部的类。

成员内部类

可以直接在类的内部定义成员内部类。

1
2
3
4
5
6
7
8
9
package org.ep;

public class Test {
public class Inner {
public void test() {
System.out.println("内部成员");
}
}
}

成员内部类和成员方法、成员变量一样,是对象所有的,而不是类所有的,如果我们要使用成员内部类,那么就需要:

1
2
3
4
5
6
7
8
package org.ep;

public class Main {
public static void main(String[] args) {
Test test = new Test(); //首先创建对象
Test.Inner inner = test.new Inner(); //成员内部类的类型就是外层+内部类名称
}
}

我门同样可以使用成员内部类的方法:

1
2
3
4
5
6
7
8
9
package org.ep;

public class Main {
public static void main(String[] args) {
Test test = new Test(); //首先创建对象
Test.Inner inner = test.new Inner(); //成员内部类的类型就是外层+内部类名称
inner.test();
}
}
1
内部成员

成员内部类也可以使用访问权限控制,如果我们我们将其权限改为private,那么就像我们把成员变量访问权限变成私有一样,外部是无法访问到这个内部类的。

在成员内部类中,是可以访问到外层的变量的,因为成员内部类本身就是某个对象所有的,每个对象都有这样的一个类定义,而name也是属于这个对象的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//Test.java
package org.ep;

public class Test {
private final String name;

public Test(String name) {
this.name = name;
}
public class Inner {
public void test() {
System.out.println(name);
}
}
}
1
2
3
4
5
6
7
8
9
10
//Main.java
package org.ep;

public class Main {
public static void main(String[] args) {
Test test = new Test("E");
Test.Inner inner = test.new Inner();
inner.test();
}
}
1
E

每个类可以创建一个对象,每个对象都有一个单独的类定义,可以通过这个成员内部类又创建出更多对象。

如果内部类中出现重名对象,依然遵循就近原则,可以使用this关键字,但也只能表示内部类对象,如果想要访问为外部对象,需要在前面添加外部类型名称。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package org.ep;

public class Test {
private final String name;

public Test(String name) {
this.name = name;
}
public class Inner {
String name;
public void test(String name) {
System.out.println(name); //这里的name是内部类的方法的参数
System.out.println(this.name); //内部类对象
System.out.println(Test.this.name); //访问外部对象
}
}
}

包括对方法的调用和super关键字的使用,也是类似的:

1
2
3
4
5
6
7
8
9
public class Inner {
String name;
public void test(String name){
this.toString(); //内部类自己的toString方法
super.toString(); //内部类父类的toString方法
Test.this.toString(); //外部类的toSrting方法
Test.super.toString(); //外部类父类的toString方法
}
}

所以,成员内部类其实在某些情况下使用起来比较麻烦,对于这种成员内部类,我们一般只会在类的内部自己使用。

静态内部类

前面的成员内部类类似于成员变量和成员方法,是属于对象的。同样的,静态内部类类似于静态变量和静态方法,是属于类的,可以直接创建使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.ep;

public class Test {
private final String name;

public Test(String name) {
this.name = name;
}
public static class Inner {
public void test() {
System.out.println("静态内部类");
}
}
}

不需要任何对象,可以直接创建静态内部类的对象:

1
2
3
4
5
6
7
8
package org.ep;

public class Main {
public static void main(String[] args) {
Test.Inner inner = new Test.Inner(); //可以直接创建了
inner.test();
}
}

静态内部类由于是静态的,所以相对外部来说,整个内部类中都处于静态上下文是无法访问到外部类的非静态内容的,但内部不受影响。

局部内部类

局部内部类就像局部变量一样,可以在方法中定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.ep;

public class Test {
private final String name;

public Test(String name){
this.name = name;
}

public void hello(){
class Inner { //直接在方法中创建局部内部类

}
}
}

作用范围只能在方法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.ep;

public class Test {
public void hello(){
class Inner{ //局部内部类跟局部变量一样,先声明后使用
public void test(){
System.out.println("局部内部类");
}
}

Inner inner = new Inner(); //局部内部类直接使用类名就行
inner.test();
}
}

使用频率很低,几乎不会用到。

匿名内部类

匿名内部类通常用于创建一个简单的、只在一个地方使用的类。我们可以在需要的地方直接创建匿名内部类的实例,而无需显式地编写类的定义。匿名内部类通常在方法参数中或者局部变量中被使用。

在前面的抽象类和接口中,我们不能直接通过new的方式去实现一个抽象类或者是接口对象,但是可以使用匿名内部类:

1
2
3
4
5
package org.ep;

public abstract class Student {
public abstract void test();
}

正常情况下,要创建一个抽象类的实例对象,只能对其进行继承,通过子类实现抽象类的方法。而通过在方法中使用匿名内部类,将其中的抽象方法实现,并直接创建实例对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
package org.ep;

public class Main {
public static void main(String[] args) {
Student student = new Student() { //在new的时候,后面加上花括号,把未实现的方法实现了
@Override
public void test() {
System.out.println("匿名内部类");
}
};
student.test();
}
}
1
匿名内部类

此时这里创建出来的Student对象,就是一个已经实现了抽象方法的对象,这个抽象类直接就定义好了,甚至连名字都没有,就可以直接就创出对象。

匿名内部类中同样可以使用类中的属性(因为它本质上就相当于是对应类型的子类)所以说内部还可以定义属性:

1
2
3
4
5
6
7
8
Student student = new Student() {
int a; //因为本质上就相当于是子类,所以说子类定义一些子类的属性完全没问题

@Override
public void test() {
System.out.println(name + "匿名内部类"); //直接使用父类中的name变量
}
};

同样,接口也可以通过这种匿名内部类的形式,直接创建一个匿名的接口实现类:

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
Study study = new Study() {
@Override
public void study() {
System.out.println("study");
}
};
study.study();
}

不是只有抽象类和接口可以创建匿名内部类,普通的类也可以,只不过意义不大。

Lambda表达式

如果一个接口中有且仅有一个待实现的抽象方法,那么我们可以将匿名内部类简写成Lambda表达式:

1
2
3
4
5
6
7
8
package org.ep;

public class Main {
public static void main(String[] args) {
Study study = ()-> System.out.println("studying");
study.study();
}
}

Lambda表达式的具体规范:

  • 标准格式为:([参数类型 参数名称,]...) ‐> { 代码语句,包括返回值 }
  • 和匿名内部类不同,Lambda仅支持接口,不支持抽象类。
  • 接口内部必须有且仅有一个抽象方法(可以有多个方法,但是必须保证其他方法有默认实现,必须留一个抽象方法出来)

比如我们实现的Study接口:

1
2
3
4
5
6
package org.ep;

public interface Study {

void study();
}

只需要实现一个无参无返回值的方法,所以Study study = ()-> System.out.println("studying");就是最简单的Lambda表达式。

如果需要有参数与返回值的话:

1
2
3
4
5
6
package org.ep;

public interface Study {

String study(int a);
}

我们需要使用形式完整的Lambda表达式:

1
2
3
4
5
6
7
8
9
10
11
package org.ep;

public class Main {
public static void main(String[] args) {
Study study = (a) ->{
System.out.println("study");
return "study" + a; //实际上这里面就是方法体
};
System.out.println(study.study(10));
}
}
1
2
study
study10

如果方法体里只有一个返回语句,可以直接省去花括号和return关键字:

1
2
3
Study study = (a) ->{
return "study" + a;
};

简化为:

1
Study study = (a) -> "study" + a;

如果参数只有一个,可以去掉小括号:

1
Study study = a -> "study" + a;

如果一个方法的参数需要的是一个接口的实现:

1
2
3
4
5
6
7
8
9
10
11
package org.ep;

public class Main {
public static void main(String[] args) {
test(a -> "今天学会了"+a); //参数直接写成lambda表达式
}

private static void test(Study study){
study.study(10);
}
}

方法引用

方法引用就是将一个已实现的方法,直接作为接口中抽象方法的实现(当然前提是方法定义得一样才行)。

1
2
3
4
5
6
package org.ep;

public interface Study {

int sum(int a, int b); //待实现的求和方法
}

使用的时候可以直接使用Lambda表达式:

1
2
3
4
5
6
7
8
9
10
package org.ep;

import java.util.Scanner;

public class Main {
public static void main(String[] args) {
Study study = (a, b) -> a + b;
System.out.println(study.sum(3, 4));
}
}
1
7

只不过还可以更简单,因为Integer类中默认提供了求两个int值之和的方法,而且参数、返回值和功能都和我们需要的方法一样,我们可以将已有方法的实现直接拿来当做接口的实现:

1
2
3
4
5
6
7
8
9
10
package org.ep;

import java.util.Scanner;

public class Main {
public static void main(String[] args) {
Study study = (a, b) -> Integer.sum(a, b); //直接使用Integer为我们实现好的求和方法
System.out.println(study.sum(3, 4));
}
}

使用::进行方法引用:

1
2
3
4
5
6
7
8
9
10
package org.ep;

import java.util.Scanner;

public class Main {
public static void main(String[] args) {
Study study = Integer::sum; //使用双冒号来进行方法引用,静态方法使用 类名::方法名 的形式
System.out.println(study.sum(3, 4));
}
}

任何方法都可以通过方法引用作为实现:

1
2
3
4
5
6
package org.ep;

public interface Study {

String study();
}

方法引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.ep;

import java.util.Scanner;

public class Main {
public static void main(String[] args) {
Main main = new Main();
Study study = main::enen;
System.out.println(study.study());
}

public String enen() {
return "enen";
}
}
1
enen

因为现在只需要一个String类型的返回值,由于String的构造方法在创建对象时也会得到一个String类型的结果,所以说:

1
2
3
public static void main(String[] args) {
Study study = String::new; //没错,构造方法也可以被引用,使用new表示
}

异常机制

1
2
3
4
5
6
7
8
9
package org.ep;
public class Main {
public static void main(String[] args) {
test(1, 0);
}
private static int test(int a, int b) {
return a / b;
}
}
1
2
3
Exception in thread "main" java.lang.ArithmeticException: / by zero
at org.ep.Main.test(Main.java:7)
at org.ep.Main.main(Main.java:4)

异常的类型

我们在之前其实已经接触过一些异常了,比如数组越界异常,空指针异常,算术异常等,他们其实都是异常类型,我们的每一个异常也是一个类,他们都继承自Exception类。

异常类型本质依然是类的对象,但是异常类型支持在程序运行出现问题时抛出(也就是上面出现的红色报错)。也可以提前声明,告知使用者需要处理可能会出现的异常!

运行时异常

异常的第一种类型是运行时异常,如上述的列子,在编译阶段无法感知代码是否会出现问题,只有在运行的时候才知道会不会出错(正常情况下是不会出错的),这样的异常称为运行时异常,异常也是由类定义的,所有的运行时异常都继承自RuntimeException

1
2
3
4
5
6
7
package org.ep;
public class Main {
public static void main(String[] args) {
String str = null;
str.toString();
}
}
1
2
Exception in thread "main" java.lang.NullPointerException
at org.ep.Main.main(Main.java:5)

又比如下面的情况:

1
2
3
4
5
6
7
package org.ep;
public class Main {
public static void main(String[] args) {
Object object = new Object();
Main main = (Main) object;
}
}
1
2
Exception in thread "main" java.lang.ClassCastException: java.lang.Object cannot be cast to org.ep.Main
at org.ep.Main.main(Main.java:5)

编译时异常

异常的另一种类型是编译时异常,编译时异常明确指出可能会出现的异常,在编译阶段就需要进行处理(捕获异常)。必须要考虑到出现异常的情况,如果不进行处理,将无法通过编译!默认继承自Exception类的异常都是编译时异常。

1
protected native Object clone() throws CloneNotSupportedException;

比如Object类中定义的clone方法,就明确指出了在运行的时候会出现的异常。

还有一种类型是错误,错误比异常更严重,是致命问题,一般出现错误可能JVM就无法继续正常运行了,比如OutOfMemoryError就是内存溢出错误(内存占用已经超出限制,无法继续申请内存了)

1
2
3
4
5
6
7
8
9
10
package org.ep;
public class Main {
public static void main(String[] args) {
test();
}

private static void test(){
test();
}
}

这样一个无限递归的方法,会导致运行过程中无限制地向下调用方法,导致栈溢出:

1
2
3
4
5
6
7
8
Exception in thread "main" java.lang.StackOverflowError
at org.ep.Main.test(Main.java:8)
at org.ep.Main.test(Main.java:8)
at org.ep.Main.test(Main.java:8)
at org.ep.Main.test(Main.java:8)
at org.ep.Main.test(Main.java:8)
at org.ep.Main.test(Main.java:8)
...

这种情况就是错误了,导致程序无法继续运行。又比如:

1
2
3
public static void main(String[] args) {
Object[] objects = new Object[Integer.MAX_VALUE]; //申请一个超级大数组
}
1
2
Exception in thread "main" java.lang.OutOfMemoryError: Requested array size exceeds VM limit
at org.ep.Main.main(Main.java:4)

此时没有更多的可用内存供我们的程序使用,那么程序也就没办法继续运行下去了,这同样是一个很严重的错误。

自定义异常

异常其实就两大类,一个是编译时异常,一个是运行时异常,我们先来看编译时异常。

1
2
3
4
5
public class TestException extends Exception{
public TestException(String message){
super(message); //这里我们选择使用父类的带参构造,这个参数就是异常的原因
}
}

编译时异常只需要继承Exception就行了,编译时异常的子类有很多很多,仅仅是SE中就有700多个,不同的异常对应着不同的情况,比如在类型转换时出错那么就是类型转换异常,如果是使用一个值为null的变量调用方法,那么就会出现空指针异常。

而运行时异常只需要继承RuntimeException就行了:

1
2
3
4
5
public class TestException extends RuntimeException{
public TestException(String message){
super(message);
}
}

RuntimeException继承自ExceptionException继承自Throwable

当然还有一种类型是Error,它是所有错误的父类,同样是继承自Throwable的。

抛出异常

当别人调用我们的方法时,如果传入了错误的参数导致程序无法正常运行,这时我们就可以手动抛出一个异常来终止程序继续运行下去,同时告知上一级方法执行出现了问题:

1
2
3
4
5
6
7
8
9
10
11
12
package org.ep;
public class Main {
public static void main(String[] args) {
test(1, 0);
}

private static int test(int a, int b) {
if (b == 0)
throw new RuntimeException("被除数不能为0");
return a / b;
}
}

异常的抛出同样需要创建一个异常对象出来,我们抛出异常实际上就是将这个异常对象抛出,异常对象携带了我们抛出异常时的一些信息,比如是因为什么原因导致的异常,在RuntimeException的构造方法中我们可以写入原因。

当出现异常时:

1
2
3
Exception in thread "main" java.lang.RuntimeException: 被除数不能为0
at org.ep.Main.test(Main.java:9)
at org.ep.Main.main(Main.java:4)

程序会终止,并且会打印栈追踪信息、当前抛出的异常类型和我们刚刚自定义异常信息。

注意,如果我们在方法中抛出了一个非运行时异常,那么必须告知函数的调用方我们会抛出某个异常,函数调用方必须要对抛出的这个异常进行对应的处理才可以:

1
2
3
private static void test() throws Exception {    //使用throws关键字告知调用方此方法会抛出哪些异常,请调用方处理好
throw new Exception("我是编译时异常!");
}

如果不同的分支条件会出现不同的异常,那么所有在方法中可能会抛出的异常都需要注明:

1
2
3
4
5
6
private static void test(int a) throws FileNotFoundException, ClassNotFoundException {  //多个异常使用逗号隔开
if(a == 1)
throw new FileNotFoundException();
else
throw new ClassNotFoundException();
}

并不是只有非运行时异常可以像这样明确指出,运行时异常也可以,只不过不强制要求:

1
2
3
private static void test(int a) throws RuntimeException {
throw new RuntimeException();
}

我们在重写方法时,如果父类中的方法表明了会抛出某个异常,只要重写的内容中不会抛出对应的异常我们可以直接省去:

1
2
3
4
@Override
protected Object clone() {
return new Object();
}

异常处理

当程序没有按照我们理想的样子运行而出现异常时(默认会交给JVM来处理,JVM发现任何异常都会立即终止程序运行,并在控制台打印栈追踪信息)现在我们希望能够自己处理出现的问题,让程序继续运行下去,就需要对异常进行捕获,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
package org.ep;

public class Main {
public static void main(String[] args) {
try { //使用try-catch语句进行异常捕获
Object object = null;
object.toString();
} catch (NullPointerException e){ //因为异常本身也是一个对象,catch中实际上就是用一个局部变量去接收异常

}
System.out.println("程序继续正常运行!");
}
}

我们可以将代码编写到try语句块中,只要是在这个范围内发生的异常,都可以被捕获,使用catch关键字对指定的异常进行捕获,这里我们捕获的是NullPointerException空指针异常:

1
程序继续正常运行!

可以看到,当我们捕获异常之后,程序可以继续正常运行,并不会像之前一样直接结束掉。

注意,catch中捕获的类型只能是Throwable的子类,也就是说要么是抛出的异常,要么是错误,不能是其他的任何类型。

我们可以在catch语句块中对捕获到的异常进行处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.ep;

public class Main {
public static void main(String[] args) {
try {
Object object = null;
object.toString();
} catch (NullPointerException e) {
e.printStackTrace();
System.out.println("异常错误信息:" + e.getMessage());
}
System.out.println("程序继续正常运行");
}
}
1
2
3
4
java.lang.NullPointerException
at org.ep.Main.main(Main.java:7)
异常错误信息:null
程序继续正常运行

如果某个方法明确指出会抛出哪些异常,除非抛出的异常是一个运行时异常,否则我们必须要使用try-catch语句块进行异常的捕获,不然就无法通过编译:

1
2
3
4
5
6
7
public static void main(String[] args) {
test(10); //必须要进行异常的捕获,否则报错
}

private static void test(int a) throws IOException { //明确会抛出IOException
throw new IOException();
}

如果我们确实不想在当前这个方法中进行处理,那么我们可以继续抛给上一级:

1
2
3
4
5
6
7
public static void main(String[] args) throws IOException {  //继续编写throws往上一级抛
test(10);
}

private static void test(int a) throws IOException {
throw new IOException();
}

如果已经是主方法了,那么就相当于到顶层了,此时发生异常再往上抛出的话,就会直接交给JVM进行处理,默认会让整个程序终止并打印栈追踪信息。

如果我们要捕获的异常,是某个异常的父类,那么当发生这个异常时,同样可以捕获到:

1
2
3
4
5
6
7
8
public static void main(String[] args) throws IOException {
try {
int[] arr = new int[1];
arr[1] = 100; //这里发生的是数组越界异常,它是运行时异常的子类
} catch (RuntimeException e){ //使用运行时异常同样可以捕获到
System.out.println("捕获到异常");
}
}

当代码可能出现多种类型的异常时,我们希望能够分不同情况处理不同类型的异常,就可以使用多重异常捕获:

1
2
3
4
5
6
7
8
9
try {
//....
} catch (NullPointerException e) {

} catch (IndexOutOfBoundsException e){

} catch (RuntimeException e){

}

但是要注意一下顺序:

1
2
3
4
5
6
7
8
9
try {
//....
} catch (RuntimeException e){ //父类型在前,会将子类的也捕获

} catch (NullPointerException e) { //永远都不会被捕获

} catch (IndexOutOfBoundsException e){ //永远都不会被捕获

}

我们也可以简写为:

1
2
3
4
5
try {
//....
} catch (NullPointerException | IndexOutOfBoundsException e) { //用|隔开每种类型即可

}

如果简写的话,那么发生这些异常的时候,都会采用统一的方式进行处理了。

如果我们希望,程序运行时,无论是否出现异常,都会在最后执行任务,可以交给finally语句块来处理:

1
2
3
4
5
6
7
try {
//....
}catch (Exception e){

}finally {
System.out.println("enen"); //无论是否出现异常,都会在最后执行
}

try语句块至少要配合catchfinally中的一个:

1
2
3
4
5
6
try {
int a = 10;
a /= 0;
} finally { //不捕获异常,程序会终止,但在最后依然会执行下面的内容
System.out.println("enen");
}

数学工具类

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
//Math也是java.lang包下的类,所以说默认就可以直接使用
System.out.println(Math.pow(5, 3)); //我们可以使用pow方法直接计算a的b次方

Math.abs(-1); //abs方法可以求绝对值
Math.max(19, 20); //快速取最大值
Math.min(2, 4); //快速取最小值
Math.sqrt(9); //求一个数的算术平方根
}

三角函数

1
2
3
4
5
6
7
Math.sin(Math.PI / 2);     //求π/2的正弦值,这里我们可以使用预置的PI进行计算
Math.cos(Math.PI); //求π的余弦值
Math.tan(Math.PI / 4); //求π/4的正切值

Math.asin(1); //三角函数的反函数也是有的,这里是求arcsin1的值
Math.acos(1);
Math.atan(0);

对数

1
2
3
4
5
6
7
public static void main(String[] args) {
Math.log(Math.E); //e为底的对数函数,其实就是ln,我们可以直接使用Math中定义好的e
Math.log10(100); //10为底的对数函数
//利用换底公式,我们可以弄出来任何我们想求的对数函数
double a = Math.log(4) / Math.log(2); //这里是求以2为底4的对数,log(2)4 = ln4 / ln2
System.out.println(a);
}

上下取整

1
2
3
4
public static void main(String[] args) {
Math.ceil(4.5); //通过使用ceil来向上取整
Math.floor(5.6); //通过使用floor来向下取整
}

生成随机数:使用Random类来生成(这个类是java.util包下的,需要手动导入才可以)

1
2
3
4
5
6
public static void main(String[] args) {
Random random = new Random(); //创建Random对象
for (int i = 0; i < 30; i++) {
System.out.print(random.nextInt(100)+" "); //nextInt方法可以指定创建0 - x之内的随机数
}
}