本系列文章:
Java(一)数据类型、变量类型、修饰符、运算符
Java(二)分支循环、数组、字符串、方法
Java(三)面向对象、封装继承多态、重写和重载、枚举
Java(四)内部类、包装类、异常、日期
Java(五)反射、克隆、泛型、语法糖
Java(六)IO
Java(七)Lambda、Stream、新日期类、Optional
将一个类的定义放在里另一个类的内部,就是内部类
。可以将内部类看作类的一个属性,与其他属性定义方式一致。
内部类可以分为四种:成员内部类、静态内部类、局部内部类、匿名内部类。
成员内部类是最常见的内部类,即一个类中嵌套了另一个类,无特殊修饰符。成员内部类的语法:
new 外部类().new 内部类()
成员内部类示例:
package Inner;
public class OutClass {
private int outerVariable = 1;
private int commonVariable = 2;
private static int outerStaticVariable = 3;
public class Inner {
private int commonVariable = 20;
public Inner() {
}
public void innerShow() {
/*当和外部类属性名相同时,直接引用属性名,访问的是内部类的成员属性*/
System.out.println("内部类、外部类中变量同名时,直接访问的是内部的变量:" + commonVariable);
/*不同名情况下,内部类可直接访问外部属性*/
System.out.println("outerVariable:" + outerVariable+",outerStaticVariable:"+outerStaticVariable);
/*当和外部类属性名相同时,可通过外部类名.this.属性名来访问外部变量*/
System.out.println("内部类、外部类中变量同名时,需要用外部类类名来访问外部的变量:" + OutClass.this.commonVariable);
}
}
/*将内部类中的接口,包装成外部类中的方法,这样其他类可方便地调用内部类中的接口*/
public void outerShow() {
Inner inner = new Inner();
inner.innerShow();
}
}
测试代码:
public class InnerTest {
public static void main(String[] args){
OutClass outClass=new OutClass();
outClass.outerShow();
}
}
这个例子中可以看出内部类和外部类访问的一些简单规则:成员内部类可以访问外部类所有的变量和方法,包括静态和非静态,私有和公有。成员内部类依赖于外部类的实例,它的创建方式外部类实例.new 内部类()
。
当内部类和外部类中有相同名称的变量时,在内部类中需要用“外部类.this.变量名”的形式才能访问。
当然,在其他类中,也可以创建内部类对象,调用内部类中的方法,示例:
public class InnerTest {
public static void main(String[] args){
OutClass outer = new OutClass();
OutClass.Inner inner = outer.new Inner();
inner.innerShow();
}
}
上面测试代码的输出结果与之前测试结果一致,并且这也是创建成员内部类对象的固定格式,即:
- 先用new的方式,创建外部类对象,如OutClass outer = new OutClass();
- 然后用 “外部类类名.内部类类名 内部类变量名 = 外部类对象.new 内部类类名()” 的方式创建内部类对象。
成员内部类的特点:
成员内部类的内部不能有静态信息
。外部类要访问内部类信息,需要先创建内部类对象,才能访问
。成员内部类可以直接使用外部类的任何信息,如果属性或者方法同名,调用外部类.this.属性或者方法
。 定义在类内部的静态类,就是静态内部类。在静态内部类中,只能访问外部类中static方法和static变量
,其他用法与成员内部类相似。静态内部类实例创建的语法:
new 外部类.静态内部类()
静态内部类示例:
/*外部类*/
public class OutClass {
private int outerVariable = 1;
private int commonVariable = 2;
private static int outerStaticVariable = 3;
static {
System.out.println("OutClass-->静态块");
}
public static void outerStaticMethod() {
System.out.println("外部类-->静态方法");
}
public static class Inner {
private int innerVariable = 10;
private int commonVariable = 20;
static {
System.out.println("Inner-->静态块");
}
private static int innerStaticVariable = 30;
public void innerShow() {
System.out.println("内部类中变量innerVariable:" + innerVariable);
System.out.println("内部类中与外部类同名变量commonVariable:" + commonVariable);
System.out.println("外部类中变量outerStaticVariable:"+outerStaticVariable);
outerStaticMethod();
}
public static void innerStaticShow() {
//被调用时会先加载Outer类
//outerStaticMethod();
}
}
public static void callInner() {
System.out.println(Inner.innerStaticVariable);
Inner.innerStaticShow();
}
}
/*测试类*/
public class InnerTest {
public static void main(String[] args) {
//访问静态内部类的静态方法,Inner类被加载,此时外部类未被加载,独立存在,不依赖于外围类。
OutClass.Inner.innerStaticShow();
//访问静态内部类的成员方法
// OutClass.Inner oi = new OutClass.Inner();
// oi.innerShow();
}
}
此时的测试结果为:
Inner–>静态块
从这个例子可以看出,当静态内部类不访问外部类中的static变量或static方法时,是不会调用外部类的静态代码块的
。将上述外部类代码稍微修改:
public class OutClass {
private int outerVariable = 1;
private int commonVariable = 2;
private static int outerStaticVariable = 3;
static {
System.out.println("OutClass-->静态块");
}
public static void outerStaticMethod() {
System.out.println("外部类-->静态方法");
}
public static class Inner {
private int innerVariable = 10;
private int commonVariable = 20;
static {
System.out.println("Inner-->静态块");
}
private static int innerStaticVariable = 30;
public void innerShow() {
System.out.println("内部类中变量innerVariable:" + innerVariable);
System.out.println("内部类中与外部类同名变量commonVariable:" + commonVariable);
System.out.println("外部类中变量outerStaticVariable:"+outerStaticVariable);
outerStaticMethod();
}
public static void innerStaticShow() {
//被调用时会先加载OutClass类
outerStaticMethod();
}
}
public static void callInner() {
System.out.println(Inner.innerStaticVariable);
Inner.innerStaticShow();
}
}
此时的测试结果:
Inner–>静态块
OutClass–>静态块
外部类–>静态方法
可以看出,在内部类中访问外部static变量或static方法时,就会加载外部类的静态代码块,不过是在加载完内部类的静态代码块之后
。
静态内部类的特点:
1>静态内部类的方法只能访问外部类的static变量或static方法。
2>访问内部类的静态信息的形式是:直接外部类.内部类.静态信息。
3>静态内部类可以独立存在,不依赖于其他外部类。
局部内部类的位置
和之前的两个类不一样,不再是在一个类内部,而是在方法内部
。
定义在实例方法中的局部类可以访问外部类的所有变量和方法,定义在静态方法中的局部类只能访问外部类的静态变量和方法
。
局部内部类的使用,和之前的两种内部类差别主要有两点:
访问方法内的变量时,变量需要用final修饰
。局部内部类只能在方法内使用
。
示例:
package Inner;
/*外部类*/
public class OutClass {
private int outerVariable = 1;
private int commonVariable = 2;
private static int outerStaticVariable = 3;
public void outerMethod() {
System.out.println("外部类-->普通方法");
}
public static void outerStaticMethod() {
System.out.println("外部类-->静态方法");
}
public void outerCreatMethod(final int value) {
final boolean inOut = false;
class Inner {
private int innerVariable = 10;
private int commonVariable = 20;
public void innerShow() {
System.out.println("内部类-->变量innerVariable:" + innerVariable);
/*局部变量*/
System.out.println("是否直接在外部类中:" + inOut);
System.out.println("内部类所在方法的参数value:" + value);
/*访问外部类的变量、方法*/
System.out.println("外部类中的普通变量outerVariable:" + outerVariable);
System.out.println("访问内部类的同名变量commonVariable:" + commonVariable);
System.out.println("访问外部类的同名变量commonVariable:" + OutClass.this.commonVariable);
System.out.println("外部类中的静态变量outerStaticVariable:" + outerStaticVariable);
outerMethod();
outerStaticMethod();
}
}
/*局部内部类只能在方法内使用*/
Inner inner = new Inner();
inner.innerShow();
}
}
测试类代码如下:
public class InnerTest {
public static void main(String[] args) {
OutClass outer = new OutClass();
outer.outerCreatMethod(100);
}
}
测试结果:
内部类–>变量innerVariable:10
是否直接在外部类中:false
内部类所在方法的参数value:100
外部类中的普通变量outerVariable:1
访问内部类的同名变量commonVariable:20
访问外部类的同名变量commonVariable:2
外部类中的静态变量outerStaticVariable:3
外部类–>普通方法
外部类–>静态方法
局部内部类的特点:
1>类前不能有访问修饰符。
2>使用范围为当前方法内。
3>不能声明static变量和static方法。
4>JDK8以前(不包括8)只能访问被final修饰的变量,不论是方法接收的参数,还是方法内的参数,JDK8后隐式地加上final
。
5>可以随意的访问外部类的变量和方法。
匿名内部类就是没有名字的内部类,本质上是一个重写或实现了父类或接口的子类对象。
匿名内部类创建方式:
new 类/接口{
//匿名内部类实现部分
}
匿名内部类的使用场景:一般是只使用一次某个接口的实现类时
。示例:
/*定义一个接口*/
public interface Sport{
void play();
}
/*测试类*/
public class OutClass {
public static void main(String[] args){
OutClass.getInnerInstance("打篮球").play();
}
public static Sport getInnerInstance(final String sport){
return new Sport(){
@Override
public void play(){
System.out.println(sport);
}
};
}
}
测试结果:
打篮球
匿名内部类的特点:
1>匿名内部类无访问修饰符。
2>使用匿名内部类的主要目的重写new后的类的某个或某些方法(匿名内部类必须继承一个抽象类或者实现一个接口
)。
3>匿名内部类访问方法参数时也有和局部内部类同样的限制。
4>匿名内部类没有构造方法。
5>当所在的方法的形参需要被匿名内部类使用时,必须声明为final
。
6>匿名内部类不能定义任何静态成员和静态方法
。
7>匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法。
当然最常见的匿名内部类就是线程:
new Thread( new Runnable() {
public void run(){
System.out.println("test");
}
}).start();
间接完成功能扩展
。利用访问修饰符隐藏内部类的实施细节,提供了更好的封装
,除外部类,都不能访问。一个内部类对象可以访问创建它的外部类对象的内容,包括私有数据
;常见的是使用模板方法模式的时候,将部分接口的实现转移到子类中实现
。
- 不可能为其他的类使用;
- 出于某种原因,不能被其他类引用,可能会引起错误。
先说结论:在JDK8之前,如果我们在匿名内部类中需要访问局部变量,那么这个局部变量必须用final修饰符修饰
;在JDK8中,如果我们在匿名内部类中需要访问局部变量,那么这个局部变量不需要用final修饰符修饰,其原因是:看似是一种编译机制的改变,实际上就是一个语法糖(底层还是帮你加了final)。
这种现象的原因是:用final修饰实际上就是为了保护数据的一致性。这里所说的数据一致性,对引用变量来说是引用地址的一致性,对基本类型来说就是值的一致性。
Java是一个面向对象的语言,同时Java中存在着8种基本数据类型,为每个基本数据类型设计一个对应的类进行代表,这种方式增强了Java面向对象的性质。
很多地方都需要使用对象而不是基本数据类型。比如,在集合类中,无法将int 、double等类型放进去的,因为集合的容器要求元素是Object类型。而包装类型的存在使得向集合中传入数值成为可能,包装类的存在弥补了基本数据类型的不足
。
此外,包装类还为基本类型添加了属性和方法,丰富了基本类型的操作。比如int类型的最大值和最小值,直接用哪个Integer.MAX_VALUE和Integer.MIN_VALUE表示即可。
Java有8种基本数据类型:byte、short、int、long、float、double、boolean、char,因此包装类也有8种:
基本类型 | 包装类 |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
boolean | Character |
char | Boolean |
Number 是所有数字包装类的父类。 |
因为包装类是对象,而基本数据不是对象,所以预想中,两者应该是有转换机制的,示例:
/*基本数据类型转为包装类*/
Integer num1 = new Integer(1);
/*包装类型转为基本数据类型*/
int num2 = num1.intValue();
System.out.println("包装类值:"+num1+",基本类型值:"+num2);
所谓的自动装箱和自动拆箱,就是说不用这么明显的转换,系统会默认装换,示例:
/*自动装箱,编译器会改为 new Integer(1)*/
Integer num1 = 1;
/*自动拆箱,编译器会修改为new Integer(1).intValue()*/
int num2 = num1;
System.out.println("包装类值:"+num1+",基本类型值:"+num2); //包装类值:1,基本类型值:1
基本类型和包装类型为什么可以直接相互赋值呢?这其实是Java中的一种“语法糖”。“语法糖”是指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。
在自动装箱的时候,基本类型要与包装类类型一一对应;自动拆箱的时候,包装类类型 <= 基本类型就可以
。
自动装箱和拆箱发生的场景:
- 赋值操作(装箱或拆箱);
- 进行加减乘除混合运算 (拆箱);
- 进行>,<,==比较运算(拆箱);
- 调用equals进行比较(装箱);
- ArrayList、HashMap等集合类添加基础类型数据时(装箱)。
由于自动装箱和拆箱的操作,包装类和基本类型的互相转换其实不需要开发者手动进行。以下是相互转换的方式:
基本类型 | 基本类型–>包装类 | 包装类–>基本类型 |
---|---|---|
byte | new Byte / valueOf | byteValue |
short | new Short / valueOf | shortValue |
int | new Integer / valueOf | intValue |
long | new Long / valueOf | longValue |
float | new Float / valueOf | floatValue |
double | new Double / valueOf | doubleValue |
boolean | new Boolean / valueOf | booleanValue |
char | new Character / valueOf | charValue |
以Integer为例,我们看一下它的valueOf 方法,示例: |
public static Integer valueOf(int i) {
return i >= 128 || i < -128 ? new Integer(i) : SMALL_VALUES[i + 128];
}
从上面代码可以看出,其实包装类的valueOf方法,还是通过 ‘new 包装类()’ 的方式来创建包装类对象的。
基本类型和包装类互相转换,示例:
/*基本型转换为包装类对象*/
Byte num1 = new Byte((byte) 1);
Short num2 = new Short((short) 2);
Integer num3 = new Integer(3);
Long num4 = new Long(4);
Float num5 = new Float(5.0);
Double num6 = new Double(6.0);
Character num7 = new Character((char) 99);
Boolean bool1 = new Boolean(true);
//包装类值,Byte型:1,Short型:2,Integer型:3,Long型:4,Float型:5.0,Double型:6.0,Character型:c,Boolean型:true
System.out.println("包装类值,Byte型:"+num1+",Short型:"+num2+",Integer型:"+num3+",Long型:"+num4
+",Float型:"+num5+",Double型:"+num6+",Character型:"+num7+",Boolean型:"+bool1);
/*包装类转换为基本类型*/
byte num11 = num1.byteValue();
short num12 = num2.shortValue();
int num13 = num3.intValue();
long num14 = num4.longValue();
float num15 = num5.floatValue();
double num16 = num6.doubleValue();
char num17 = num7.charValue();
boolean bool2 = bool1.booleanValue();
//基本类型值,byte型:1,short型:2,int型:3,long型:4,float型:5.0,double型:6.0,char型:c,boolean型:true
System.out.println("基本类型值,byte型:"+num11+",short型:"+num12+",int型:"+num13+",long型:"+num14
+",float型:"+num15+",double型:"+num16+",char型:"+num17+",boolean型:"+bool2);
常见的形式是:
Integer num1 = null;
int num2 = num1;
此时运行代码,会提示空指针,原因是:将num1的值赋给num2时,会先进行自动拆箱,也就是num1.intValue(),此时num1是null,所以报了空指针异常。
先看个例子:
Integer int1 = 1;
Integer int2 = 1;
System.out.println(int1 == int2); //true
Integer int3 = 200;
Integer int4 = 200;
System.out.println(int3 == int4); //false
用int值创建Integer对象时,有个默认装箱的操作,不过对int的值是有要求的:
public static Integer valueOf(int i) {
// 判断实参是否在可缓存范围内,默认为[-128, 127]
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
从这段源码可以看出,当用[-128, 127]范围内的值作为参数创建Integer对象时,会创建相同的对象
;否则会创建出不同的对象。
起始Java基本类型的包装类,大部分都实现了常量池技术,即Byte、Short、Integer、Long、Character、Boolean,前4种包装类默认创建了数值[-128,127]的相应类型的缓存数据,Character创建了数值在[0,127]范围的缓存数据,Boolean直接返回True或False。如果超出这些范围,就会正常地创建新的对象。
两种浮点数类型的包装类Float、Double并没有实现常量池技术。
Java异常,就是程序出现了预期之外的情况,这个出现异常的时间可能是编译期或运行期。Java中,针对这种意外情况,存在一种专业的机制来处理:异常处理机制
。Java异常是Java提供的一种识别及响应错误的一致性机制,该机制的最大作用是让程序尽可能恢复到正常状态。
Java异常机制可以使程序中异常处理代码和正常业务代码分离,保证程序代码更加优雅,并提高程序健壮性。在有效使用异常的情况下,异常能清晰的回答what, where, why这3个问题:异常类型回答了“什么”被抛出,异常堆栈跟踪回答了“在哪”抛出,异常信息回答了“为什么”会抛出。
Throwable是Java中处理异常情况的最顶级父类,该类下面有两个子类:Error和Exception。
Error是错误,对于所有的编译时期的错误以及系统错误都是通过Error抛出的。这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如Java虚拟机运行错误、类定义错误等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。
Exception是另外一个非常重要的异常子类,Exception规定的异常是程序本身可以处理的异常
。异常可以分为编译时异常或者检查时异常。
Throwable是所有错误与异常的超类。
Throwable包含两个子类:Error(错误)和 Exception(异常),它们通常用于指示发生了异常情况。
Throwable包含了其线程创建时线程执行堆栈的快照,它提供了printStackTrace()等接口用于获取堆栈跟踪数据等信息。
Throwable 类常用方法:
//返回异常发生时的详细信息
public string getMessage()
//返回异常发生时的简要描述
public string toString()
//返回异常对象的本地化信息。使用Throwable 的子类覆盖这个方法,可以生成本地化信息。
//如果子类没有覆盖该方法,则该方法返回的信息与getMessage()返回的结果相同
public string getLocalizedMessage()
//在控制台上打印Throwable对象封装的异常信息
public void printStackTrace()
定义:Error 类及其子类。程序中无法处理的错误,表示运行应用程序中出现了严重的错误
。
特点:此类错误一般表示代码运行时JVM出现问题。通常有 Virtual MachineError(虚拟机运行错误)、NoClassDefFoundError(类定义错误)、OutOfMemoryError:内存不足错误;StackOverflowError:栈溢出错误。此类错误发生时,JVM 将终止线程。
当此类错误发生时,应用程序不应该去处理此类错误。按照Java惯例,开发者是不应该实现任何新的Error子类的。
程序本身可以捕获并且可以处理的异常。Exception 这种异常又分为两类:运行时异常和编译时异常。
RuntimeException 类及其子类,表示 JVM 在运行期间可能出现的异常
。数组索引越界异常
。当对数组的索引值为负数或大于等于数组大小时抛出。示例: int[] arr = {1,2,3,4,5};
for(int i=0;i<=arr.length;i++) {
System.out.println(arr[i]);
}
算术条件异常
。譬如:整数除零等。示例: System.out.println(5/0);
非法参数异常
,进行非法调用时,传递的参数不合规。 示例: Date day = new Date();
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String date = df.format(day);
SimpleDateFormat dateFormat= new SimpleDateFormat("yyyy-MM");
String format = dateFormat.format(date);
System.out.println(format);
空指针异常
。当应用试图在要求使用对象的地方使用了null时,抛出该异常。譬如:调用null对象的实例方法、访问null对象的属性、计算null对象的长度、使用throw语句抛出null等等。示例: String str = null;
System.out.println(str.length());
索引越界异常
。当访问某个序列的索引值小于0或大于等于序列大小时,抛出该异常。示例: ArrayList<String> arrayList = new ArrayList<>();
System.out.println(arrayList.get(2));
类转型异常
。假设有类A和B(A不是B的父类或子类),O是A的实例,那么当强制将O构造为类B的实例时抛出该异常。该异常经常被称为强制类型转换异常。示例:public interface Animal {
abstract void eat();
}
public class Cat implements Animal {
@Override
public void eat() {
System.out.println("吃鱼");
}
public void catchMouse() {
System.out.println("抓老鼠");
}
}
public class Dog implements Animal{
@Override
public void eat() {
System.out.println("吃骨头");
}
public void watchHouse() {
System.out.println("看家");
}
}
//测试类
public class JavaTest {
public static void main(String[] args) throws IOException {
Animal a = new Cat();
a.eat();
Cat c = (Cat)a;
c.catchMouse();
Dog d = (Dog)a;
d.watchHouse(); // ClassCastException异常
}
}
public class Person {
public void methodOne(String s){
System.out.println("调用了public methodOne方法");
}
}
Class perClass = null;
try {
perClass = Class.forName("com.test.Person");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
perClass.getMethods();
Method[] methodArray = perClass.getMethods();
Method m;
try {
m = perClass.getMethod("methodTwo", String.class);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
File file = new File("F:/123.txt");
OutputStream outputStream = null;
try {
outputStream = new FileOutputStream(file);
outputStream.close();
outputStream.write("456".getBytes());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
File f0 = new File("F:/kkk.out");
FileInputStream fis = null;
FileOutputStream fos = null;
ObjectInputStream dis = null;
ObjectOutputStream dos = null;
try{
if(!f0.exists())f0.createNewFile();
fos = new FileOutputStream(f0);
fis = new FileInputStream(f0);
// 1. 初始化Object流语句
dis = new ObjectInputStream(fis);
dos = new ObjectOutputStream(fos);
// 2. 写"对象"语句
dos.writeInt(1);
dos.writeObject(new Integer(3));
// 3. 读取,输出语句
System.out.println(dis.readInt() + ","+ dis.readInt());
} catch (Exception e){
e.printStackTrace();
if(fos != null) fos.close();
if(fis != null) fis.close();
if(dos != null) dos.close();
if(dis != null) dis.close();
}
Class perClass = null;
try {
perClass = Class.forName("com.test.Person1");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
运行时异常包括 RuntimeException 类及其子类,表示 JVM 在运行期间可能出现的异常
。 Java 编译器不会检查运行时异常。
受检异常是Exception 中除 RuntimeException 及其子类之外的异常
。 Java 编译器会检查受检异常。
RuntimeException异常和受检异常之间的区别:是否强制要求调用者必须处理此异常。如果强制要求调用者必须进行处理,那么就使用受检异常,否则就选择非受检异常(RuntimeException)。一般来讲,如果没有特殊的要求,我们建议使用RuntimeException异常。
Java异常机制重用到的关键字:
关键字 | 作用 |
---|---|
try | try后面的{ }中,是有可能抛出异常的代码块。如果这些代码块中出现了异常,就可以被及时发现,进行下一阶段处理 |
catch | 用于捕获异常。catch后的{ }中,是针对某一类异常的具体处理 |
finally | 不管代码运行时有没有异常,finally后的{ }语句总会被执行,一般用于一些IO的终止操作等。 |
throw | 在代码中主动抛出异常 |
throws | 用于声明一个方法可能抛出的异常 |
常见的语句有两种:try…catch和try…catch…finally,通用一点的写法是:
try {
可能出现异常的代码
} catch(异常类名A e){
如果出现了异常类A类型的异常,那么执行该代码
} ...(catch可以有多个){
}finally {
最终肯定必须要执行的代码(例如释放资源的代码)
}
此时有两种情况:try代码块中出现了异常和没出现异常。
public static void main(String[] args){
int[] arr = {1,2,3,4,5};
try {
for(int i=0;i<=arr.length;i++)
System.out.println(arr[i]);
System.out.println("try代码块中的数组元素输出完毕");
} catch (ArrayIndexOutOfBoundsException e) {
e.printStackTrace();
System.out.println("进入catch代码块");
}finally{
System.out.println("进入finally代码块");
}
}
public static void main(String[] args){
int[] arr = {1,2,3,4,5};
try {
for(int i=0;i<arr.length;i++)
System.out.println(arr[i]);
System.out.println("try代码块中的数组元素输出完毕");
} catch (ArrayIndexOutOfBoundsException e) {
e.printStackTrace();
System.out.println("进入catch代码块");
}finally{
System.out.println("进入finally代码块");
}
}
异常类存在子父类的关系,那么子类应该在前,父类在后
。如果程序可能存在多种异常,需要多个catch进行捕获
。 在try和finally块中都抛出异常时,finally块中的异常会覆盖掉try块中的异常
。为了解决这种情形,JDK1.7引入了try-with-resource机制,这种机制可以实现资源的自动释放,自动释放的资源需要是实现了AutoCloseable接口的类
。示例:
private static void tryWithResourceTest(){
try (Scanner scanner = new Scanner(new FileInputStream("c:/abc"),"UTF8")){
// code
} catch (IOException e){
// handle exception
}
}
在上面的代码中,当try代码块中抛出异常时,会自动调用scanner.close方法,和把scanner.close方法放在finally代码块中不同的是,若scanner.close抛出异常,则会被抑制,抛出的仍然为原始异常。被抑制的异常会由addSusppressed方法添加到原来的异常,如果想要获取被抑制的异常列表,可以调用getSuppressed方法来获取。
被抑制的异常会出现在抛出的异常的堆栈信息中,也可以通过getSuppressed方法来获取这些异常。这样做的好处是不会丢失任何异常,方便开发人员进行调试。
通常,应该捕获那些知道如何处理的异常,将不知道如何处理的异常继续传递下去。传递异常可以在方法签名处使用throws关键字声明可能会抛出的异常
。
如果你觉得解决不了某些异常问题,且不需要调用者处理,那么你可以抛出异常。
throw关键字作用是在方法内部抛出一个Throwable类型的异常
。任何Java代码都可以通过throw语句抛出异常。
程序通常在运行之前不报错,但是运行后可能会出现某些未知的错误,但是还不想直接抛出到上一级,那么就需要通过try…catch…的形式进行异常捕获,之后根据不同的异常情况来进行相应的处理。
如果原生的异常类不能满足功能要求,开发者可以写自己的异常类。如果要自定义非检查异常,则继承RuntimeException;如果要自定义检查异常,则继承Exception
。此处可以借鉴一下IOException的写法:
public class IOException extends Exception {
private static final long serialVersionUID = 7818375828146090155L;
public IOException() {
}
public IOException(String detailMessage) {
super(detailMessage);
}
public IOException(String message, Throwable cause) {
super(message, cause);
}
public IOException(Throwable cause) {
super(cause == null ? null : cause.toString(), cause);
}
}
此处以Student为例,如果Student的分数不在[1,100]范围内,我们就抛出一个异常。示例:
/*自定义异常类*/
public class GradeException extends Exception{
public GradeException() {
}
public GradeException(String detailMessage) {
super(detailMessage);
}
public GradeException(String message, Throwable cause) {
super(message, cause);
}
public GradeException(Throwable cause) {
super(cause == null ? null : cause.toString(), cause);
}
}
/*学生类*/
public class Student {
private String name;
private int grade;
public Student(){
}
public String getName(){
return this.name;
}
public void setName(String name){
if(name.length()!=0){
this.name = name;
}
}
public int getGrade(){
return this.grade;
}
public void setGrade(int grade) throws GradeException {
if(grade > 100 || grade < 0){
throw new GradeException("分数参数不合法,应该是0-100的整数");
}else{
this.grade = grade;
}
}
}
/*测试类*/
public class BasicTest {
public static void main(String[] args){
Student student = new Student();
try {
student.setGrade(101);
} catch (GradeException e) {
e.printStackTrace();
}
}
}
简单来说,三者区别:
finalize()是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法,供垃圾收集时的其他资源回收,例如关闭文件等
。再详细地说,finalize()方法在垃圾收集器将对象从内存中清除出去前,做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没被引用时对这个对象调用的。它是在Object类中定义的,因此所的类都继承了它。子类覆盖finalize()方法以整理系统资源或者执行其他清理工作。finalize()方法是在垃圾收集器删除对象之前对这个对象调用的。
- 在定义的时候初始化。
- final变量可以在初始化块中初始化,不可以在静态初始化块中初始化。
- 静态final变量可以在定义时初始化,也可以在静态初始化块中初始化,不可以在初始化块中初始化。
- final变量还可以在类的构造器中初始化,但是静态final变量不可以。
try{
throw new NullPointerException();
}catch(NullPointerException e){
System.out.println("程序抛出了异常");
}finally{
System.out.println("执行了finally语句块");
}
测试结果为:
程序抛出了异常
执行了finally语句块
public class BasicTest {
public static void main(String[] args) {
// 测试return语句对finally块代码执行的影响
testReturn();
System.out.println();
// 测试continue语句对finally块代码执行的影响
testContinue();
System.out.println();
// 测试break语句对finally块代码执行的影响
testBreak();
}
static ReturnClass testReturn() {
try {
return new ReturnClass();
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("testReturn方法中,执行了finally语句");
}
return null;
}
static void testContinue(){
for(int i=0; i<3; i++){
try {
System.out.println(i);
if(i == 1){
System.out.println("con");
}
} catch(Exception e) {
e.printStackTrace();
} finally {
System.out.println("testContinue方法中,执行了finally语句");
}
}
}
static void testBreak() {
for (int i=0; i<3; i++) {
try {
System.out.println(i);
if (i == 1) {
break;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("testBreak方法中,执行了finally语句");
}
}
}
}
public class ReturnClass {
public ReturnClass() {
System.out.println("创建ReturnClass对象");
}
}
测试结果:
很明显,return、continue和break都没能阻止finally语句块的执行。从结果上直观地看,return语句似乎在finally语句块之前执行了,其实不然,return语句的作用是退出当前的方法,并将值或对象返回。如果 finally语句块是在return语句之后执行的,那么return语句被执行后就已经退出当前方法了,finally语句块执行不了。
因此,正确的执行顺序应该是这样的:编译器在编译return new ReturnClass();时,将它分成了两个步骤,new ReturnClass()和return,前一个创建对象的语句是在finally语句块之前被执行的,而后一个return语句是在finally语句块之后执行的,也就是说finally语句块是在程序退出方法之前被执行的。同样,finally语句块是在循环被跳过(continue)和中断(break)之前被执行的。
此时,可以总结一下finally与return语句的执行顺序的关系:
- 如果try块中有return,finally块的代码仍会执行,并且finally的执行早于try里面的return。
- 当try和catch中有return时,finally仍然会执行。
- finally是在return后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把要返回的值保存起来,管finally中的代码怎么样,返回的值都不会改变,任然是之前保存的值),所以函数返回值是在finally执行前确定的。
- finally中最好不要包含return,否则程序会提前退出,返回值不是try或catch中保存的返回值。
protected void finalize() throws Throwable { }
看一个调用该方法的demo:
public class BasicTest {
public static void main(String[] args) {
BasicTest bs = new BasicTest();
bs = null;
System.gc();
}
@Override
protected void finalize() throws Throwable {
System.out.println("执行了finalize方法");
}
}
测试结果为:
执行了finalize方法
finalize()方法中一般用于释放非Java 资源(如打开的文件资源、数据库连接等)。同时,finalize()方法的调用时机具有不确定性,导致开发者并不能依赖finalize()方法能及时的回收占用的资源,可能出现的情况是在耗尽资源之前,gc却仍未触发,因而通常的做法是提供显式的close()方法供客户端手动调用。
if (obj != null) {…}
反例:
try { obj.method(); } catch
(NullPointerException e) {…}
private int x = 0;
public int checkReturn() {
try {
// x等于1,此处不返回
return ++x;
} finally {
// 返回的结果是2
return ++x;
}
}
1) 返回类型为基本数据类型,return包装数据类型的对象时,自动拆箱有可能产生NPE。 反例:
public int f() { return Integer对象}, 如果为null,自动解箱抛NPE
。
2) 数据库的查询结果可能为null。
3) 集合里的元素即使isNotEmpty,取出的数据元素也可能为null。
4) 远程调用返回对象时,一律要求进行空指针判断,防止NPE。
5) 对于Session中获取的数据,建议进行NPE检查,避免空指针。
6) 级联调用obj.getA().getB().getC();一连串调用,易产生NPE。
正例:使用JDK8的Optional类来防止NPE问题。
private static void readFile(String filePath) throws MyException {
try {
// code
} catch (IOException e) {
MyException ex = new MyException("read file failed.");
ex.initCause(e);
throw ex;
}
}
根据实际的开发要求是否严格来决定。在实际的项目开发项目工作中,所有的异常是统一使用Exception处理还是分开处理,完全根据开发者的项目开发标准来决定。如果项目开发环境严谨,基本上要求针对每一种异常分别进行处理,并且要详细记录下异常产生的时间以及产生的位置,这样可以方便程序开发人员进行代码的维护。
注意:处理多个异常时,捕获范围小的异常要放在捕获范围大的异常之前处理。
throws | throw | |
---|---|---|
位置 | 用在函数上,后面跟的是异常类,可以跟多个 | 用在函数内,后面跟的是异常对象 |
功能 | 声明异常 | 抛出具体的问题对象 |
意义 | 表示出现异常的一种可能性,并不一定会发生这些异常 | 已经抛出了某种异常对象 |
两者都是消极处理异常的方式,只是抛出或者可能抛出异常,但是不会由函数去处理异常,真正的处理异常由函数的上层调用处理。
检查型异常 | 非检查型异常 | |
---|---|---|
继承谁 | Exception | RuntimeException |
处理方式 | 必须使用try…catch或者throws等关键字进行处理,否则编译器会报错 | 可以通过修改代码来规避 |
在一个方法中如果发生异常,这个方法会创建一个异常对象,并转交给 JVM,该异常对象包含异常名称,异常描述以及异常发生时应用程序的状态。创建异常对象并转交给 JVM 的过程称为抛出异常。可能有一系列的方法调用,最终才进入抛出异常的方法,这一系列方法调用的有序列表叫做调用栈。
JVM 会顺着调用栈去查找看是否有可以处理异常的代码,如果有,则调用异常处理代码。当 JVM 发现可以处理异常的代码时,会把发生的异常传递给它。如果 JVM 没有找到可以处理该异常的代码块,JVM 就会将该异常转交给默认的异常处理器(默认处理器为 JVM 的一部分),默认异常处理器打印出异常信息并终止应用程序。
catch或finally可以省略:
在finally块中清理资源或者使用try-with-resource语句
FileInputStream inputStream = null;
try {
File file = new File("./tmp.txt");
inputStream = new FileInputStream(file);
// use the inputStream to read a file
} catch (FileNotFoundException e) {
log.error(e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
log.error(e);
}
}
}
File file = new File("./tmp.txt");
try (FileInputStream inputStream = new FileInputStream(file);) {
// use the inputStream to read a file
} catch (FileNotFoundException e) {
log.error(e);
} catch (IOException e) {
log.error(e);
}
在异常处理机制中,只有匹配异常的第一个 catch 块会被执行。 因此,如果首先捕获 IllegalArgumentException ,则永远不会到达应该处理更具体的 NumberFormatException 的catch块,因为它是 IllegalArgumentException 的子类。所以应该总是优先捕获最具体的异常类,并将不太具体的 catch 块添加到列表的末尾。
异常处理的性能成本非常高,每个 Java 程序员在开发时都应牢记这句话。创建一个异常非常慢,抛出一个异常又会消耗1~5ms,当一个异常在应用的多个层级之间传递时,会拖累整个应用的性能。所以应该在这两种情况下使用异常:
至少有两种情况下finally语句是不会被执行的:
finally语句被执行的必要而非充分条件是:相应的try语句一定被执行到
。在try块中有System.exit(0);这样的语句
,System.exit(0);是终止Java虚拟机JVM的,连JVM都停止了,所有都结束了,当然finally语句也不会被执行到。异常处理的性能成本非常高,每个 Java 程序员在开发时都应牢记这句话。创建一个异常非常慢,抛出一个异常又会消耗1~5ms,当一个异常在应用的多个层级之间传递时,会拖累整个应用的性能。因此,对异常的合理使用方式是:
简单来说,Date类用来获取时间,SimpleDateFormat类用来显示时间,Calendar类用来计算时间。
java.util 包提供了Date类来封装当前的日期和时间。该类中的常用方法:
public Date()
public Date(long date)
`public long getTime()
public void setTime(long time)
上面方法的示例:
Date date = new Date();
System.out.println("当前时间:");
System.out.println(date.toString()); //Thu Oct 29 10:06:51 CST 2020
System.out.println(date.getTime()); //1603937211568
System.out.println("设置后的时间:");
date.setTime(1503937115121L);
System.out.println(date.toString()); //Tue Aug 29 00:18:35 CST 2017
System.out.println(date.getTime()); //1503937115121
该类用于日期的格式化,常用于字符串和时间类对象之间的互相转换
。示例:
SimpleDateFormat sdf =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS" );
Date d= new Date();
String str = sdf.format(d);
System.out.println(str); //2021-08-29 16:10:39 517
SimpleDateFormat sdf1 =new SimpleDateFormat("yyyy-MM-dd" );
Date d1= new Date();
String str1 = sdf1.format(d1);
System.out.println(str1); //2021-08-29
上面是日期转字符串的例子,下面看下字符串转日期的:
SimpleDateFormat sdf =new SimpleDateFormat("yyyy/MM/dd HH:mm:ss" );
String str = "2020/10/30 10:12:00";
try {
Date d = sdf.parse(str);
System.out.printf(d.toString()); //Fri Oct 30 10:12:00 CST 2020
} catch (ParseException e) {
e.printStackTrace();
}
在使用SimpleDateFormat类进行转换时,常用的转换规则为:
字符 | 含义 |
---|---|
y | 年 |
M | 月 |
d | 日 |
H | 24进制的小时 |
h | 12进制的小时 |
m | 分钟 |
s | 秒 |
S | 毫秒 |
Calendar功能比Date多一些,使用上也更复杂一些,常用的方法有:
public static Calendar getInstance()
public final void set(int year,int month,int date)
public void set(int field,int value)
public void add(int field, int amount)
关于时间的不同维度(年、月、日),Calendar中有特定的字段来表示,如下:
示例:
Calendar c1 = Calendar.getInstance();
c1.set(2009, 6 - 1, 12);
//2009,5,12
System.out.println(c1.get(Calendar.YEAR)+","
+c1.get(Calendar.MONTH)+","
+c1.get(Calendar.DATE));
c1.set(Calendar.YEAR,2008);
c1.set(Calendar.MONTH,8);
c1.set(Calendar.DATE,8);
//2008,8,8
System.out.println(c1.get(Calendar.YEAR)+","
+c1.get(Calendar.MONTH)+","
+c1.get(Calendar.DATE));
c1.add(Calendar.DATE, 10);
//2008,8,18
System.out.println(c1.get(Calendar.YEAR)+","
+c1.get(Calendar.MONTH)+","
+c1.get(Calendar.DATE));
前面介绍的Date和SimpleDateFormat是可以相互转换的,那么Date和Calendar可以相互转换吗?答案肯定是可以的。
Calendar cal1=Calendar.getInstance();
Date date1=cal1.getTime();
Date date2=new Date();
Calendar cal2=Calendar.getInstance();
cal2.setTime(date2);