(1)equals 定义 & 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 的默认实现 & 重写
默认实现:
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;
}
(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 重写
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;
// 类定义
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
(1)使用
public class CloneExample implements Cloneable {
private int a;
private int b;
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
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指向了同一个对象
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方法并遍历即可
序列化是一种处理对象流的机制,用于将对象状态转换为字节流,即将对象转换为可存储或可传输的状态。我们可以对流化后的对象进行读写操作,或在网络中传输。从字节流转换为对象的过程称为反序列化。
序列化的方式包括两种:
序列化常用场景:
注意:
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();
只要一个类实现了Serilizable接口,这个类的所有属性和方法都会自动序列化。
为了安全起见,不希望在网络操作(主要涉及到序列化操作,本地序列化缓存也适用)中被传输,这些信息对应的变量就可以加上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
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");
序列化接口 | Serializable | Parcelable |
---|---|---|
作用 | 为了保存对象的属性到本地文件、数据库、网络流、rmi以方便数据传输,当然这种传输可以是程序内的也可以是两个程序间的 | 因为Serializable效率过慢,为了在程序内不同组件间以及不同Android程序间(AIDL)高效的传输数据而设计,这些数据仅在内存中存在,Parcelable是通过IBinder通信的消息的载体 |
性能 & 场景 | Serializable可将数据持久化方便保存,所以在需要保存或网络传输数据时选择Serializable | Parcelable的性能比Serializable好,在内存开销方面较小,所以在内存间数据传输时推荐使用Parcelable,如activity间传输数据 |
实现 | 类只需要实现Serializable接口,并提供一个序列化版本id(serialVersionUID)即可对类的所有属性和方法自动序列化 | 需要实现writeToParcel、describeContents函数以及静态的CREATOR变量,实际上就是将如何打包和解包的工作自己来定义,而序列化的这些操作完全由底层实现 |
高级功能 | 不保存静态变量,可以使用Transient关键字对部分字段不进行序列化,也可以覆盖writeObject、readObject方法以实现序列化过程自定义 |
菜鸟教程:Java 内部类详解
在Java中,定义在类内部的类被称为内部类。设计内部类的好处是:
内部类一般来说包括这四种:成员内部类、局部内部类、匿名内部类和静态内部类。
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);
class Man{
public Man(){}
public People getWoman(){
class Woman extends People{ //局部内部类
int age =0;
}
return new Woman();
}
}
scan_bt.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
}
});
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异常机制可以使程序中异常处理代码和正常业务代码分离,保证程序代码更加优雅,并提高程序健壮性。
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关键字可以用来修饰类、方法和变量。
- 修饰类
当用final修饰一个类时,表明这个类不能被继承。- 修饰方法
当用final修饰一个方法时,该方法是不能被子类所覆盖的。- 修饰变量
当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中建立对象关联,则当前对象可以复活一次。
public static void main(String[] args) {
String s = "abc";
if(s.equals("abc")) {
throw new NumberFormatException();
} else {
System.out.println(s);
}
//function();
}
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();
}
}
throws | throw | |
位置 | 函数头 | 函数体 |
是否发生异常 | 表示出现异常的一种可能性,并不一定会发生这些异常 | throw则是抛出了异常,执行throw则一定抛出了某种异常对象 |
相同 | 两者都是消极处理异常的方式,只是抛出或者可能抛出异常,但是不会由函数去处理异常 |
Java异常以Throwable开始,扩展出Error和Exception。
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");
}
}
}