第三章 Object(equals、hashCode、clone) && 序列化 && 内部类 && 静态属性/方法 && Java异常 && final

Objct通用方法?

Object.equals()

(1)equals 定义 & equals 与 == 区别(等价 与 值相等)

  • ==
    基本数据类型(byte,short,char,int,long,float,double,boolean)用比较的是两个数据的值是否相等。
    引用类型(类、接口、数组)用
    比较的是它们在内存中的存放地址是否相等(两个变量是否引用同一个对象)。
    对象是存放在堆中的,栈中存放的是对象的引用(地址)。因此==是对栈中的值进行比较的。如果要比较堆中对象的内容是否相同,那么就要重写equals方法了。
  • Object.equals(Object obj)
    Object的equals方法主要用于判断引用的对象是否等价(内容相等)。
Integer x = new Integer(1);
Integer y = new Integer(1);
System.out.println(x.equals(y)); // true
System.out.println(x == y);      // false

(2)equals 的默认实现 & 重写
默认实现:

  • 检查是否为同一个对象的引用,如果是直接返回 true;
  • 检查是否是同一个类型,如果不是,直接返回 false;
  • 将 Object 对象进行转型;
  • 判断每个关键域是否相等。
public class EqualExample {

    private int x;
    private int y;
    private int z;

    public EqualExample(int x, int y, int z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        EqualExample that = (EqualExample) o;

        if (x != that.x) return false;
        if (y != that.y) return false;
        return z == that.z;
    }
}

覆盖equals方法一般通过比较对象的内容是否相等来判断对象是否相等。如下为String类对equals方法进行重写。(不同类有不同等价方法的实现)

public boolean equals(Object anObject) {
    if (this == anObject) {   // 参数是否为这个对象的引用
        return true;				// 若A==B,即是同一个String对象,返回true
    }
    if (anObject instanceof String) {	//参数是否为正确的类型(若对比对象是String类型则继续)
        String anotherString = (String)anObject;
	//获取关键域,判断关键域是否匹配
        int n = count;
        if (n == anotherString.count) {		//	判断A、B长度是否一样,不一样则返回false
        char v1[] = value;
        char v2[] = anotherString.value;
        int i = offset;
        int j = anotherString.offset;
        while (n-- != 0) {		//逐个字符比较,若有不相等字符,则返回false
            if (v1[i++] != v2[j++])
            return false;
        }
        return true;
        }
    }
    return false;
    }

Object.hashCode()

(1)定义 & 散列集合 唯一性原理
hashCode方法返回一个hash码(int),主要作用是在对对象进行散列时作为key输入,因此需要每个对象的hashCode尽可能不同,这样才能保证散列的存取性能。事实上,Object类提供的默认实现确保每个对象的hash码不同(在对象的内存地址基础上经过特定算法返回一个hashCode)
hashCode用于配合基于散列的集合一起正常运行,这样的散列集合包括HashSet、HashMap以及HashTable。散列集合中元素不可重复,Java则依据元素的hashCode来判断两个元素是否重复。当集合要添加新的元素时,先调用这个元素的hashCode方法,就一下子能定位到它应该放置的物理位置上。如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了(放入对象的hashcode与集合中任一元素的hashcode不相等);如果这个位置上已经有元素了(hashcode相等),就调用它的equals方法与新元素进行比较,相同的话就不存,不相同就散列其它的地址。(通过调用equals解决冲突)
Set/HashSet如何确保它的唯一性?

if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {... }

(2)equals 与 hashCode 重写

  • 等价的两个对象散列值一定相同
    若重写equals(Object obj)方法,有必要重写hashcode()方法,确保通过equals(Object obj)方法判断结果为true的两个对象具备相等的hashcode()返回值。
  • 散列值相同的两个对象不一定等价
    如果equals(Object obj)返回false,即两个对象“不相同”,并不要求对这两个对象调用hashcode()方法得到两个不相同的数。
    为了满足上述规范,覆盖equals方法时总要覆盖hashCode,这样该类才能结合所有基于散列的集合(如HashMap、HashSet、HashTable)一起正常运作。
    下面的代码中,新建了两个等价的对象,并将它们添加到 HashSet 中。我们希望将这两个对象当成一样的,只在集合中添加一个对象,但是因为 EqualExample 没有实现 hashCode() 方法,因此这两个对象的散列值是不同的,最终导致集合添加了两个等价的对象。
EqualExample e1 = new EqualExample(1, 1, 1);
EqualExample e2 = new EqualExample(1, 1, 1);
System.out.println(e1.equals(e2)); // true
HashSet<EqualExample> set = new HashSet<>();
set.add(e1);
set.add(e2);
System.out.println(set.size());   // 2

重写实例:Student类同时覆盖hashCode与equals方法

public class Student {
	private int age;
	private String name;
	public Student() {
	}
	public Student(int age, String name) {
		super();
		this.age = age;
		this.name = name;
	}
	public int getAge() {
		return age;
	}
	public String getName() {
		return name;
	}
	public void setAge(int age) {
		this.age = age;
	}
	public void setName(String name) {
		this.name = name;
	}
	@Override
	public int hashCode() { 
	// 重写hashCode保证相同的对象(equals)的hashCode相同,不重复存入集合
	// 采用数字31作为优质乘子
		final int prime = 31;
		int result = 1;
		// 对所有关键域与优质乘子进行运算
		result = prime * result + age;
		result = prime * result + ((name == null) ? 0 : name.hashCode());
		System.out.println("hashCode : "+ result);
		return result;
	}
	@Override
	public boolean equals(Object obj) {
	// 重写equals保证姓名、年龄相等为同一对象
	// 引用相等 => 相等
	// 类型相等 && 所有关键域相等 =>相等
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Student other = (Student) obj;
		if (age != other.age)
			return false;
		if (name == null) {
			if (other.name != null)
				return false;
		} else if (!name.equals(other.name))
			return false;
		return true;
	}
	
}

(3)散列函数构造
理想的散列函数应当具有均匀性,即不相等的对象应当均匀分布到所有可能的散列值上。这就要求了散列函数要把所有域的值都考虑进来。可以将每个域都当成 R 进制的某一位,然后组成一个 R 进制的整数。R 一般取 31,因为它是一个奇素数,如果是偶数的话,当出现乘法溢出,信息就会丢失,因为与 2 相乘相当于向左移一位。
一个数与 31 相乘可以转换成移位和减法:31*x == (x<<5)-x,编译器会自动进行这个优化。
String hashCode 方法实现:

int h = hash;
if (h == 0 && value.length > 0) {
    char val[] = value;
    for (int i = 0; i < value.length; i++) {
        h = 31 * h + val[i];
    }
    hash = h;
}
return h;

Object.toString()

// 类定义
public class ToStringExample {

    private int number;

    public ToStringExample(int number) {
        this.number = number;
    }
}
// 调用实现
ToStringExample example = new ToStringExample(123);
System.out.println(example.toString());
// 输出结果
// 默认返回 ToStringExample@4554617c 这种形式,其中 @ 后面的数值为散列码的无符号十六进制表示。
ToStringExample@4554617c

Object.clone()

(1)使用

  • 重写clone()方法
    clone() 是 Object 的 protected 方法,它不是 public,一个类不显式去重写 clone(),其它类就不能直接去调用该类实例的 clone() 方法。
  • 实现Cloneable接口
    Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException。
public class CloneExample implements Cloneable {
    private int a;
    private int b;

    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

(2)浅拷贝 & 深拷贝
在这里插入图片描述

  • 浅拷贝(super.clone())
    拷贝对象和原始对象的引用类型引用同一个对象。
    创建一个新对象,然后将当前对象的非静态字段复制到该新对象,如果字段是值类型(基本类型)的,那么对该字段值执行复制;如果该字段是引用类型(对象、String)的话,则复制引用但不复制引用的对象。因此,原始对象及其副本对象引用同一个对象,如果其中一个对象改变了这个地址,就会影响到另一个对象。
public class ShallowCloneExample implements Cloneable {
    private int[] arr;
    public void set(int index, int value) {
        arr[index] = value;
    }
    @Override
    protected ShallowCloneExample clone() throws CloneNotSupportedException {
        return (ShallowCloneExample) super.clone();
    }
}
ShallowCloneExample e1 = new ShallowCloneExample();
ShallowCloneExample e2 = null;
try {
    e2 = e1.clone();
} catch (CloneNotSupportedException e) {
    e.printStackTrace();
}
e1.set(2, 222);
System.out.println(e2.get(2)); // 222

可以看出e1与e2的arr指向了同一个对象

  • 深拷贝(clone() 代替方案)
    拷贝对象和原始对象的引用类型引用不同对象。
    深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。
    (1)实现Cloneable接口进行深拷贝
    实现clone方法,并且在clone方法内部,把该对象引用的其他对象也要clone一份 , 这就要求这个被引用的对象必须也要实现Cloneable接口并且实现clone方法。
rotected Person clone() {
        Person person = null;
        try {
            person = (Person) super.clone();
            person.setEmail(new Email(person.getEmail().getObject(),person.getEmail().getContent()));
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }   
        return person;
    }

(2)自定义拷贝函数
使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。Effective Java 书上讲到,最好不要去使用 clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。

public class CloneConstructorExample {

    private int[] arr;

    public CloneConstructorExample() {
        arr = new int[10];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = i;
        }
    }

    public CloneConstructorExample(CloneConstructorExample original) {
        // 为引用类型重新在堆中分配内存
        // 使拷贝对象与原始对象的引用类型指向不同的对象
        arr = new int[original.arr.length];
        for (int i = 0; i < original.arr.length; i++) {
            arr[i] = original.arr[i];
        }
    }

    public void set(int index, int value) {
        arr[index] = value;
    }

    public int get(int index) {
        return arr[index];
    }
}
CloneConstructorExample e1 = new CloneConstructorExample();
CloneConstructorExample e2 = new CloneConstructorExample(e1);
e1.set(2, 222);
System.out.println(e2.get(2)); // 2

(3)利用序列化完成对象深拷贝
将原对象(需要实现Serializable接口)写入到一个字节流中(outputStream.writeObject),再从字节流中将其读取出来创建一个新的对象(inputStream.readObject)

public class CloneUtils {
    @SuppressWarnings("unchecked")
    public static <T extends Serializable> T clone(T obj){
        T cloneObj = null;
        try {
            //写入字节流
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            ObjectOutputStream obs = new ObjectOutputStream(out);
            obs.writeObject(obj);
            obs.close();
            //分配内存,写入原始对象,生成新对象
            ByteArrayInputStream ios = new ByteArrayInputStream(out.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(ios);
            //返回生成的新对象
            cloneObj = (T) ois.readObject();
            ois.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return cloneObj;
    }
}

(3)如何深拷贝一个List集合
Json拷贝法:将List集合映射为一个Json字符串,再解析成一个List集合
序列化法:将List集合写入字节流,再读取出来解析成一个List集合
重写clone法:重写clone方法并遍历即可

序列化

序列化是一种处理对象流的机制,用于将对象状态转换为字节流,即将对象转换为可存储或可传输的状态。我们可以对流化后的对象进行读写操作,或在网络中传输。从字节流转换为对象的过程称为反序列化。
序列化的方式包括两种:

  1. 实现Serializable接口(Java自带)
  2. 实现Parcelable接口(Android专用、效率更高)
    Parcelable方式的实现原理是将一个完整的对象进行分解,而分解后的每一部分都是Intent所支持的数据类型,从而实现传递对象的功能。

序列化常用场景:

  1. 将对象状态存储到文件或数据库中
  2. 在网络上传输对象
  3. 在Activity/Fragment之间通过Intent传递对象(Android)

注意:

  1. 静态成员变量属于类不属于对象,所以不会参与序列化(对象序列化保存的是对象的“状态”,也就是它的成员变量,因此序列化不会关注静态变量)
  2. 用transient关键字标记的成员变量不参与序列化(在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null)

Serializable接口

import java.io.Serializable;

public class StudentSerializable implements Serializable {
    
    // 指定serialVersionUID,
    // 因为原则上序列化后的数据中的serialVersionUID只有和当前类的serialVersionUID相同时才能被正常的反序列化
    private static final long serialVersionUID = 10000000000000000L; 
    
    private int Uid;
    private String Name ;
    
    public int getUid() {
        return Uid;
    }
    public void setUid(int uid) {
        Uid = uid;
    }
    public String getName() {
        return Name;
    }
    public void setName(String name) {
        Name = name;
    }
    @Override
    public String toString() {
        return "StudentSerializable [Uid=" + Uid + ", Name=" + Name + "]";
    }
}

// 使用 文件传输
oos = new ObjectOutputStream(new FileOutputStream(fullFilename.getAbsoluteFile()));
oos.writeObject(stu);

ois = new ObjectInputStream(new FileInputStream(fullFilename.getAbsoluteFile()));
StudentSerializable newStu = (StudentSerializable) ois.readObject();

transient 关键字

只要一个类实现了Serilizable接口,这个类的所有属性和方法都会自动序列化。
为了安全起见,不希望在网络操作(主要涉及到序列化操作,本地序列化缓存也适用)中被传输,这些信息对应的变量就可以加上transient关键字。换句话说,这个字段的生命周期仅存于调用者的内存中而不会写到磁盘里持久化。

  • 一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问。
  • transient关键字只能修饰变量,而不能修饰方法、类。
  • 一个静态变量不管是否被transient修饰,均不能被序列化。
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

/**
 * @description 使用transient关键字不序列化某个变量
 *        注意读取的时候,读取数据的顺序一定要和存放数据的顺序保持一致
 */
public class TransientTest {

    public static void main(String[] args) {

        User user = new User();
        user.setUsername("Alexia");
        user.setPasswd("123456");

        System.out.println("read before Serializable: ");
        System.out.println("username: " + user.getUsername());
        System.err.println("password: " + user.getPasswd());

        try {
            ObjectOutputStream os = new ObjectOutputStream(
                    new FileOutputStream("C:/user.txt"));
            os.writeObject(user); // 将User对象写进文件
            os.flush();
            os.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            // 在反序列化之前改变username的值
            User.username = "jmwang";

            ObjectInputStream is = new ObjectInputStream(new FileInputStream(
                    "C:/user.txt"));
            user = (User) is.readObject(); // 从流中读取User的数据
            is.close();

            System.out.println("\nread after Serializable: ");
            System.out.println("username: " + user.getUsername());
            System.err.println("password: " + user.getPasswd());

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

class User implements Serializable {
    private static final long serialVersionUID = 8294180014912103005L;  

    public static String username;
    private transient String passwd;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPasswd() {
        return passwd;
    }

    public void setPasswd(String passwd) {
        this.passwd = passwd;
    }

}

输出

read before Serializable: 
username: Alexia
password: 123456

read after Serializable: 
username: jmwang
password: null

Parcelable接口

import android.os.Parcel;
import android.os.Parcelable;

public class StudentParcelable implements Parcelable{
    
    private int Uid;
    private String Name ;
    private Book book ;
   
	// 省略 构造函数 & getter & setter

    //功能:返回当前对象的内容描述,如果含有文件描述符,返回1
    //即CONTENTS_FILE_DESCRIPTOR
    //几乎所有情况都会返回0
    @Override
    public int describeContents() {
        // TODO Auto-generated method stub
        return 0;
    }

    /**
     * 序列化功能由writeToParcel完成,最终通过Parcel的一系列Write方法完成
     */
    //功能:将当前对象写入序列化结构中,其中flags标识有两种值,0或1
    //为1时标识当前对象需要作为返回值返回,不能立刻释放资源,即PARCELABLE_WRITE_RETURN_VALUE
    //不过几乎所有情况都为0
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        // TODO Auto-generated method stub
        dest.writeInt(Uid);
        dest.writeString(Name);
        dest.writeParcelable(book, 0);
    }
    
    /**
     * 反序列化由CREATOR来完成,其内部标明了如何创建序列化对象和数组
     * 并通过Parcel的一系列read方法来完成反序列化
     */
    public StudentParcelable(Parcel source){
        Uid = source.readInt();
        Name = source.readString();
        
        //注意:book是一个可序列化对象,所以它的反序列化过程需要传递当前线程的上下文类加载器
        //否则会报找不到类的错误
        book = source.readParcelable(Thread.currentThread().getContextClassLoader());
    }
    
    public static final Parcelable.Creator<StudentParcelable> CREATOR = new Parcelable.Creator<StudentParcelable>() {

        //功能: 从Parcel容器中读取传递数据值,封装成Parcelable对象返回逻辑层。
        @Override
        public StudentParcelable createFromParcel(Parcel source) {
            // TODO Auto-generated method stub
            return new StudentParcelable(source);
        }

        //功能:创建一个类型为T,长度为size的数组,仅一句话(return new T[size])即可。方法是供外部类反序列化本类数组使用。
        @Override
        public StudentParcelable[] newArray(int size) {
            // TODO Auto-generated method stub
            return new StudentParcelable[size];
        }
    }; 
}

/// 使用 组件间(Activity)数据传输
Intent intent = new Intent(this,Second.class);
StudentParcelable stu = new StudentParcelable(001,"fish");
intent.putExtra("student", stu);
startActivity(intent);

Intent intent = getIntent();
StudentParcelable stu = (StudentParcelable) intent.getParcelableExtra("student");
序列化接口SerializableParcelable
作用为了保存对象的属性到本地文件、数据库、网络流、rmi以方便数据传输,当然这种传输可以是程序内的也可以是两个程序间的因为Serializable效率过慢,为了在程序内不同组件间以及不同Android程序间(AIDL)高效的传输数据而设计,这些数据仅在内存中存在,Parcelable是通过IBinder通信的消息的载体
性能 & 场景Serializable可将数据持久化方便保存,所以在需要保存或网络传输数据时选择SerializableParcelable的性能比Serializable好,在内存开销方面较小,所以在内存间数据传输时推荐使用Parcelable,如activity间传输数据
实现类只需要实现Serializable接口,并提供一个序列化版本id(serialVersionUID)即可对类的所有属性和方法自动序列化需要实现writeToParcel、describeContents函数以及静态的CREATOR变量,实际上就是将如何打包和解包的工作自己来定义,而序列化的这些操作完全由底层实现
高级功能不保存静态变量,可以使用Transient关键字对部分字段不进行序列化,也可以覆盖writeObject、readObject方法以实现序列化过程自定义

内部类

菜鸟教程:Java 内部类详解
在Java中,定义在类内部的类被称为内部类。设计内部类的好处是:

  • 更好地封装:内部类中的属性和方法与其他类隔离,且即时是外部类也不能直接访问,相反内部类可以直接访问外部类的属性和方法。
  • 实现多继承:每个内部类都能独立地实现接口。无论外部类是否已实现了某个接口,对内部类都没有影响。

内部类一般来说包括这四种:成员内部类、局部内部类、匿名内部类和静态内部类。

  1. 成员内部类:成员内部类定义为位于另一个类的内部,类似于一个成员。成员内部类可以无条件访问外部类的所有成员属性和成员方法(内部类编译后会默认持有外部类对象的一个引用,因此可以在成员内部类中随意访问外部类的成员。)。成员内部类是依附外部类而存在的,因此要创建成员内部类的对象,前提是必须存在一个外部类的对象(否则所持有的外部类对象的引用无法初始化赋值,也无法创建内部类对象)。
class Circle {	// 外部类
	
    private double radius = 0;
 
    public Circle(double radius) {
        this.radius = radius;
        // 外部类如果想调用内部类的成员方法,必须先创建成员内部类的对象,再进行访问
        getDrawInstance().drawSahpe();   
    }
     
    private Draw getDrawInstance() {
        return new Draw();
    }
     
    class Draw {     //内部类
        public void drawSahpe() {
            System.out.println(radius);  //成员内部类可以无条件访问外部类的所有成员属性和成员方法(包括private成员和静态成员)
        }
    }
}

public class Main{
	public static void main(String[] args)  {
        //成员内部类是依附外部类而存在的,因此要创建成员内部类的对象,前提是必须存在一个外部类的对象
        Circle circle = new Circle(5);
        Circle.Draw draw = circle.new Draw();
        draw.drawShape();
    }
}

编译器在进行编译的时候,会将成员内部类单独编译成一个字节码文件:
在这里插入图片描述
反编译 Outter$Inner.class 文件得到下面信息:

// 编译器会默认为成员内部类添加了一个指向外部类对象的引用
final com.cxh.test2.Outter this$0;
// 编译器会为内部类的构造方法默认添加一个参数,该参数的类型为指向外部类对象的一个引用
// 所以成员内部类中的 Outter this&0 指针便指向了外部类对象,因此可以在成员内部类中随意访问外部类的成员。
public com.cxh.test2.Outter$Inner(com.cxh.test2.Outter);
  1. 局部内部类:局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内;
class Man{
    public Man(){}
     
    public People getWoman(){
        class Woman extends People{   //局部内部类
            int age =0;
        }
        return new Woman();
    }
}
  • 为什么局部内部类访问局部变量需要final?
    局部内部类引用局部变量,不添加final,会出现生命周期不同,导致非法引用问题。因为非final的局部变量的生命周期比局部内部类的生命周期短,当方法执行结束,非final的局部变量会被销毁,而局部内部类对局部变量的引用依然存在,当局部内部类调用局部变量时,会出错,出现非法引用。
    而且直接拷贝会出现数据不同步问题,所以使用final(若定义为final,即拷贝了一个变量的副本,提供给局部内部类,这个副本的生命周期和局部内部类一样长,并且这个副本不可修改,保证了数据的同步)。
    故使用final可以保证合法引用,而且数据不可修改。
  1. 匿名内部类:匿名内部类是直接使用 new 来生成一个对象的引用,创建匿名内部类时它会立即创建一个该类的实例,且仅能被使用一次,所以匿名内部类是不能够被重复使用;
scan_bt.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        // TODO Auto-generated method stub
         
    }
});
  1. 静态内部类:用static修饰的内部类。静态内部类是不需要依赖于外部类的,不需要创建外部类,便能创建内部类。并且它不能使用外部类的非static成员变量或者方法,因为外部类的非static成员必须依附于具体的对象。(不持有外部类对象的引用)
public class Test {
    public static void main(String[] args)  {
    // 不需要存在外部类对象
        Outter.Inner inner = new Outter.Inner();
    }
}
 
class Outter {
    public Outter() {
         
    }
     
    static class Inner {
        public Inner() {
             
        }
    }
}

Java 异常

Java中异常体系是Java提供的一种定位错误以及响应错误的一种机制。
Java异常机制可以使程序中异常处理代码和正常业务代码分离,保证程序代码更加优雅,并提高程序健壮性。

Java 异常关键字

  • try
    用于监听。将要被监听的代码(可能抛出异常的代码)放在try语句块之内,当try语句块内发生异常时,异常就被抛出。
  • catch
    用于捕获异常。catch用来捕获try语句块中发生的异常。
  • finally
    finally语句块总是会被执行。它主要用于回收在try块里打开的物理资源(如数据库连接、网络连接和磁盘文件)。只有finally块,执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了return或者throw等终止方法的语句,则就不会跳回执行,直接停止。
    (1)被finally控制的语句体一定会执行(特殊情况:在执行到finally之前jvm退出,如:System.exit(0)),用于释放资源,在IO流操作和数据库操作中会见到。
    (2)finally块中的内容会先于try中的return语句执行(return执行前会将finally语句执行完再执行return 进行返回)
    public static void main(String[] args) {
        try {
            int i = 10/0;
              System.out.println("i="+i); 
        } catch (ArithmeticException e) {
              System.out.println("Caught Exception"); 
            System.out.println("e.getMessage(): " + e.getMessage()); 
            System.out.println("e.toString(): " + e.toString()); 
            System.out.println("e.printStackTrace():");
            e.printStackTrace(); 
        }finally{
			System.out.println("run finally");
	}
    }

// 输出
Caught Exception
e.getMessage(): / by zero
e.toString(): java.lang.ArithmeticException: / by zero
e.printStackTrace():
java.lang.ArithmeticException: / by zero
    at Demo1.main(Demo1.java:6)
run finally

在try语句块中有除数为0的操作,该操作会抛出java.lang.ArithmeticException异常。通过catch,对该异常进行捕获。
观察结果我们发现,并没有执行System.out.println(“i=”+i)。这说明try语句块发生异常之后,try语句块中的剩余内容就不会再被执行了。
finally语句块中的语句总是会执行。

  • 面试:final/finally/finalize的作用?
  • finally修饰符(关键字)
    在Java中,final关键字可以用来修饰类、方法和变量。
  1. 修饰类
    当用final修饰一个类时,表明这个类不能被继承。
  2. 修饰方法
    当用final修饰一个方法时,该方法是不能被子类所覆盖的。
  3. 修饰变量
    当final修饰一个基本数据类型时,表示该基本数据类型的值一旦在初始化后便不能发生变化;如果final修饰一个引用类型时,则在对其初始化之后便不能再让其指向其他对象了,但该引用所指向的对象的内容是可以发生变化的。本质上是一回事,因为引用的值是一个地址,final要求值,即地址的值不发生变化。
    final变量必须要初始化,其初始化可在变量定义时直接赋值,也可在构造函数中赋值或者作为参数传递。且在之后的引用中只读取使用,无法修改。
  • finally(异常处理)
    finally关键字一般用于异常处理中。finally结构使代码总会执行,不关有无异常发生。
    finally在try,catch中可以有,可以没有。如果trycatch中有finally则必须执行finally块中的操作。一般情况下,用于关闭文件的读写操作,或者是关闭数据库的连接等等。
  • finalize(垃圾回收)
    finalize方法是Object提供的的实例方法,通过调用finalize()方法在垃圾收集器将对象从内存中清理出去之前做必要的清理工作。
    finaliza方法执行流程:
    当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖或finalize()方法已被调用,则直接将其回收(没有必要执行)。否则,将该对象放入F-Queue队列,由一低优先级Finalizer线程执行该队列中对象的finalize方法。(但并不承诺等待运行结束)执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活”。
    对象"复活": finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那么在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那么基本上它就真的被回收了。
    因此,finalize()并不是必须要执行的,它只能执行一次或者0次。如果在finalize中建立对象关联,则当前对象可以复活一次。
  • throw
    throw是语句抛出一个异常,一般是在代码块的内部,当程序出现某种逻辑错误时由程序员主动抛出某种特定类型的异常。
public static void main(String[] args) { 
    String s = "abc"; 
    if(s.equals("abc")) { 
      throw new NumberFormatException(); 
    } else { 
      System.out.println(s); 
    } 
    //function(); 
}
  • throws
    当某个方法可能会抛出某种异常时用于throws 声明可能抛出的异常,然后交给上层调用它的方法程序处理。
public class testThrows(){
public static void function() throws NumberFormatException { 
	String s = "abc"; 
	Double d = Double.parseDouble(s); 
} 
 
public static void main(String[] args) { 
	try { 
		function(); 
	} catch (NumberFormatException e) { 
		System.err.println("非数据类型不能强制类型转换。"); 
		//e.printStackTrace(); 
	} 
}
throwsthrow
位置函数头函数体
是否发生异常表示出现异常的一种可能性,并不一定会发生这些异常throw则是抛出了异常,执行throw则一定抛出了某种异常对象
相同两者都是消极处理异常的方式,只是抛出或者可能抛出异常,但是不会由函数去处理异常

Java 异常体系

在这里插入图片描述
Java异常以Throwable开始,扩展出Error和Exception。

  • Throwable
    Throwable是 Java 语言中所有错误或异常的超类。Throwable包含两个子类: Error 和 Exception。它们通常用于指示发生了异常情况。
    Throwable包含了其线程创建时线程执行堆栈的快照,它提供了printStackTrace()等接口用于获取堆栈跟踪数据等信息。
  • Error
    Error是程序代码无法处理的错误,当资源不足、约束失败、或是其它程序无法继续运行的条件发生时,就产生错误。比如OutOfMemoryError、ThreadDeath等。和运行时异常一样,编译器也不会对错误进行检查。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止退出,其表示程序在运行期间出现了十分严重、不可恢复的错误,应用程序只能中止运行。
  • Exception
    Exception及其子类是 Throwable 的一种形式,它指出了合理的应用程序想要捕获的条件。分为运行时异常和非运行时异常:
  • 运行时异常(不检查异常)
    运行时异常都是RuntimeException类及其子类异常,是那些可能在 Java 虚拟机正常运行期间抛出的异常的超类。编译器不会检查RuntimeException。程序代码中自行选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序代码应该从逻辑角度尽可能避免这类异常的发生。
    举例常见运行时异常
  1. java.lang.NullPointerException
    空指针异常,调用了未经初始化的对象或者是不存在的对象(null对象)。
  2. java.lang.ClassNotFoundException
    指定的类不存在异常,一般类的名称和路径不正确。
  3. java.lang.ArrayIndexOutOfBoundsException
    数组下标越界异常,调用的下标超出了数组的范围。
  4. java.lang.NoSuchMethodError
    方法不存在错误,当应用试图调用某类未定义的某个方法。
  5. java.lang.IndexOutOfBoundsException
    索引越界异常。当访问某个序列的索引值小于0或大于等于序列大小时,抛出该异常。
  6. java.lang.NumberFormatException(extends IllegalArgumentException )
    数字格式异常。当试图将一个String转换为指定的数字类型,而该字符串确不满足数字类型要求的格式时,抛出该异常。
  7. java.sql.SQLException
    Sql语句执行异常
  8. java.io.IOException
    输入输出异常
  9. java.lang.IllegalArgumentException
    方法参数错误
  10. java.lang.IllegalAccessException
    无访问权限异常
  • 非运行时异常(检查异常)
    所有继承Exception且不是RuntimeException的异常都是非运行时异常,也称检查异常,如上图中的IOException和ClassNotFoundException,编译器会对其作检查,故程序中一定会对该异常进行处理,处理方法要么在方法体中声明抛出checked Exception,要么使用catch语句捕获checked Exception进行处理,不然不能通过编译。

自定义Java异常

  • 为这个异常找一个近似的类作为父类;
  • 再该类中编写两个构造器:(1)默认构造器(2)带message参数的构造器
class MyException extends Exception {
    public MyException() {}
    public MyException(String msg) {
        super(msg);
    }
}

public class Demo3 {

    public static void main(String[] args) {
        try {
            test();
        } catch (MyException e) {
            System.out.println("Catch My Exception");
            e.printStackTrace();
        }
    }
    public static void test() throws MyException{
        try {
            int i = 10/0;
              System.out.println("i="+i); 
        } catch (ArithmeticException e) {
            throw new MyException("This is MyException"); 
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李一恩

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值