clone

Java中要想自定义类的对象可以被复制,自定义类就必须实现Cloneable中的clone()方法,如下:

  1. public class Person implements Cloneable {
  2. private String name;
  3. private Animal pet;
  4. public Person(String name) {
  5. this.name = name;
  6. }
  7. public String getName() {
  8. return this.name;
  9. }
  10. public void setName(String name) {
  11. this.name = name;
  12. }
  13. public Animal getPet() {
  14. return this.pet;
  15. }
  16. public void setPet(Animal pet) {
  17. this.pet = pet;
  18. }
  19. @Override
  20. public Object clone() throws CloneNotSupportedException {
  21. return super.clone();
  22. }
  23. public String toString() {
  24. return "Person[ name: " + name + ", pet: " + pet.toString() + "]";
  25. }
  26. }
public class Animal {
    private String name;

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

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Anmial[ name: " + name + "]";
    }
}

public class Test {

    public static void main(String[] args) {
        Person ming = new Person("小明");
        Animal kitty = new Animal("喵");
        ming.setPet(kitty);
        System.out.println(ming.toString());
        try {
            Person hong = (Person) ming.clone(); // 复制ming
            hong.setName("小红");
            hong.getPet().setName("汪");
            System.out.println(hong.toString());
            System.out.println(ming.toString());
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }
}


image.png
结果发现,hong的名字可以改,不会影响ming,但是,改了hong的宠物的名字后,ming的宠物的名字也随之变了。


浅拷贝


调用Object类中clone()方法产生的效果是:先在内存中开辟一块和原始对象一样的空间,然后原样拷贝原始对象中的内 容。对基本数据类型,这样的操作是没有问题的,但对非基本类型变量,我们知道它们保存的仅仅是对象的引用(引用指向堆内存中同一个对象),这也导致clone后的非基本类型变量和原始对象中相应的变量指向的是同一个对象。因此,ming和hong指向的实际是同一个Animal类对象。

这就是浅拷贝。

那么怎么能使得拷贝得到的原始对象中的非基本类型变量也是一个新的对象(内存地址不同,内容相同)呢?这就要深拷贝。

深拷贝

上面的例子要实现深拷贝需要完成两点:

Animal类也要实现clone方法;
Person类的clone方法里,拷贝一个克隆人后,还要调用Animal的clone(),拷贝当前Person的宠物,然后将克隆宠物赋给克隆人的宠物,然后再返回这个真正的克隆人。
代码:

public class Person implements Cloneable {
    private String name;
    private Animal pet;

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

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Animal getPet() {
        return this.pet;
    }

    public void setPet(Animal pet) {
        this.pet = pet;
    }

    @Override
    public Object clone() throws CloneNotSupportedException {
        Person clonePerson = (Person) super.clone();
        clonePerson.pet = (Animal) pet.clone();
        return clonePerson;
    }

    public String toString() {
        return "Person[ name: " + name + ", pet: " + pet.toString() + "]";
    }
}
public class Animal implements Cloneable{
    private String name;

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

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

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

    public String toString() {
        return "Anmial[ name: " + name + "]";
    }
}
public class Test {

    public static void main(String[] args) {
        Person ming = new Person("小明");
        Animal kitty = new Animal("喵");
        ming.setPet(kitty);
        System.out.println(ming.toString());
        try {
            Person hong = (Person) ming.clone(); // 复制ming
            hong.setName("小红");
            hong.getPet().setName("汪");
            System.out.println(hong.toString());
            System.out.println(ming.toString());
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }
}


运行结果:
image.png

equals & == & hashCode

euqls & ==


equals 方法是基类 Object 的方法,我们创建的所有的对象都拥有这个方法,并有权利去重写这个方法。该方法返回一个 boolean 值,代表比较的两个对象是否相同。

判断相等,还有一个方法是使用==,equals和==有什么区别呢?

其实它们两个都是通过比较内存地址(准确的说是堆内存地址)来比较两个数据是否相等,但是为什么实际使用时好像不一样呢,这是因为equals很重要的一个特点,可以被重写。
先举一个字符串的例子:

public class Test {

    public static void main(String[] args) {
        String str1 = "123";
        String str2 = "123";
        String str3 = new String("123");
        String str4 = new String("123");
        System.out.println("str1==str2: " + (str1 == str2));
        System.out.println("str3==str4: " + (str3 == str4));
        System.out.println("str1==str3: " + (str1 == str3));
        System.out.println("str1.equals(str2): " + str1.equals(str2));
        System.out.println("str3.equals(str4): " + str3.equals(str4));
        System.out.println("str1.equals(str3): " + str1.equals(str3));
    }
}


运行结果:
image.png
使用==的三组字符串比较结果为什么是不同的?

前面说到,==比较的是数据的内存地址,使用用new()来创建对象时,对象会存放在堆中,每调用一次就会创建一个新的对象,因此堆内存地址不同;而第二种形如String str = “abc”,是先在栈中创建一个对String类的对象引用变量str,然后查找堆中有没有存放String对象”abc”,如果没有,则将”abc”存放进堆,并令str 指向”abc”,如果已经有”abc”,则直接令str 指向”abc”。因此str1和str2指向同一个值为”123”的String对象,而str3和str4则分别指向指向两个值为”123”的String对象,地址不同,str1和str3自然也不同。和字符串一样,也有这两种创建方式的还有,上一节提到的基本数据类型的包装类型。

那equals的比较结果为什么都是true呢?因为Java String类的equals方法重写了,它比较的两个字符串的内容是否相等,符合大多数时候人们对于字符串比较的需求。Java中equals方法重写了的还有Integer, Data等等。我们也可以根据自己的需求重写当前类的equals方法。

hashCode


hash 法简介
hash 算法,又被成为散列算法,基本上,哈希算法就是将对象本身的键值,通过特定的数学函数运算或者使用其他方法,转化成相应的数据存储地址的。

在常见的 hash 函数中有一种最简单的方法叫「除留余数法」,操作方法就是将要存入数据除以某个常数后,使用余数作为索引值。

例如:
将 323 ,458 ,25 ,340 ,28 ,969, 77 使用「除留余数法」存储在长度为11的数组中。我们假设上边说的某个常数即为数组长度11。每个数除以11后的余数即为该数在数组中的位置。
则现在想要拿到 77 ,只需要 访问arr[77%11] 就可以了。

hashCode 方法
Java 中的有所的对象都拥有的hashCode 方法其实就是一种 hash 算法,只是有的类覆写好提供给我们了,有些就需要我们手动去覆写。

hashCode 方法的作用和意义:

在 Java 中 hashCode 的存在主要是用于提高容器查找和存储的快捷性,如 HashSet, Hashtable,HashMap 等,hashCode是用来在散列存储结构中确定对象的存储地址的。

当容器中数据很多时,通过hashCode可以快速查找数据(如上面所说的除留余数法),存储数据时也可快速查看要存储的数据是否已经存在(查重)。

要注意的是,重复的数据hashCode一定相等,但是hashCode相等的数据不一定相同。还以上面的除留余数法相同,,77除以11都余0,但是,除以11余0的不一定是77。碰撞的概率与具体的算法有关。而hashset在查重的时候则是先用hashcode来缩小寻找范围,最后还要用equals()来确定是否真的为相同的对象。

wait

使调用该方法的线程释放共享资源锁,然后从运行状态退出,进入等待队列,直到被再次唤醒。

wait方法会让线程进入阻塞状态,并且会释放线程占有的锁,并交出CPU执行权限。

notify

随机唤醒等待队列中等待同一共享资源的一个线程,并使该线程退出等待队列,进入可运行状态,也就是notify()方法仅通知一个线程。

使用

1、wait()、notify/notifyAll() 方法是Object的本地final方法,无法被重写。
2、wait()使当前线程阻塞,前提是 必须先获得锁,一般配合synchronized 关键字使用,即,一般在synchronized 同步代码块里使用 wait()、notify/notifyAll() 方法。
3、 由于 wait()、notify/notifyAll() 在synchronized 代码块执行,说明当前线程一定是获取了锁的。
当线程执行wait()方法时候,会释放当前的锁,然后让出CPU,进入等待状态。
只有当 notify/notifyAll() 被执行时候,才会唤醒一个或多个正处于等待状态的线程,然后继续往下执行,直到执行完synchronized 代码块的代码或是中途遇到wait() ,再次释放锁。
也就是说,notify/notifyAll() 的执行只是唤醒沉睡的线程,而不会立即释放锁,锁的释放要看代码块的具体执行情况。所以在编程中,尽量在使用了notify/notifyAll() 后立即退出临界区,以唤醒其他线程让其获得锁

4、wait() 需要被try catch包围,以便发生异常中断也可以使wait等待的线程唤醒。
5、notify 和wait 的顺序不能错,如果A线程先执行notify方法,B线程在执行wait方法,那么B线程是无法被唤醒的。
6、notify 和 notifyAll的区别

notify方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。

notifyAll 会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系统的实现。如果当前情况下有多个线程需要被唤醒,推荐使用notifyAll 方法。比如在生产者-消费者里面的使用,每次都需要唤醒所有的消费者或是生产者,以判断程序是否可以继续往下执行。

wait-notify实现通知机制

import java.util.LinkedList;

/**
 *  生产者和消费者的问题
 *  wait、notify/notifyAll() 实现
 */
public class Storage1 implements AbstractStorage {
    //仓库最大容量
    private final int MAX_SIZE = 100;
    //仓库存储的载体
    private LinkedList list = new LinkedList();

    //生产产品
    public void produce(int num){
        //同步
        synchronized (list){
            //仓库剩余的容量不足以存放即将要生产的数量,暂停生产
            while(list.size()+num > MAX_SIZE){
                System.out.println("【要生产的产品数量】:" + num + "\t【库存量】:"
                        + list.size() + "\t暂时不能执行生产任务!");

                try {
                    //条件不满足,生产阻塞
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            for(int i=0;i<num;i++){
                list.add(new Object());
            }

            System.out.println("【已经生产产品数】:" + num + "\t【现仓储量为】:" + list.size());

            list.notifyAll();
        }
    }

    //消费产品
    public void consume(int num){
        synchronized (list){

            //不满足消费条件
            while(num > list.size()){
                System.out.println("【要消费的产品数量】:" + num + "\t【库存量】:"
                        + list.size() + "\t暂时不能执行生产任务!");

                try {
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            //消费条件满足,开始消费
            for(int i=0;i<num;i++){
                list.remove();
            }

            System.out.println("【已经消费产品数】:" + num + "\t【现仓储量为】:" + list.size());

            list.notifyAll();
        }
    }
}

LockSupport实现通知机制

LockSupport

getClass


java有两个获得类型类的方法:getClass()和class()。然后再调用该类的方法可以获取该类的相关信息,比如父类的类型类getSuperclass(),该类的名字getName()。

这两个方法涉及到了java中的反射和类型类。

反射
反射,可以理解为在运行时期获取对象类型信息的操作。

类型类
类型类指的是代表一个类型的类,因为一切皆是对象,类型也不例外,在Java使用类型类来表示一个类型。所有的类型类都是Class类的实例。

getClass()和class()最直接的区别就是,getClass() 是一个类的实例所具备的方法,而class() 方法是一个类的方法。
另外getClass() 是在运行时才确定的 (反射) ,而class() 方法是在编译时就确定了。

public class Test {

    public static void main(String[] args) {
        Cat kitty = new Cat("喵");
        Animal doggy = new Animal("汪");
        System.out.println("kitty的类型类:" + kitty.getClass() + " ; Cat的类型类:" + Cat.class);
        System.out.println("doggy的类型类:" + doggy.getClass() + " ; Animal的类型类:" + Animal.class);
        System.out.println("kitty的类名:" + kitty.getClass().getName() + " ; Cat的类名:" + Cat.class.getName());
        System.out.println("kitty父类的类型类:" + kitty.getClass().getSuperclass());
    }
}


结果:image.png

toString


toString()是Object类的一个公有方法,而所有类都继承自Object类。所以所有类即使不实现toString方法,也会存在从Object类继承来的toString,也可以重写。

而八种基本数据类型没有toString()方法,只能使用相应的包装类才能使用toString()。

例子:

public class Test {

    public static void main(String[] args) {
        Cat kitty = new Cat("喵");
        System.out.println(new Double(1.2).toString()); // 1.2
        System.out.println(new Integer(12).toString()); // 12
        System.out.println(new Boolean(true)); // true
        System.out.println(new Character('a').toString()); // a
        System.out.println(kitty.toString()); // Cat@14ae5a5
        System.out.println(kitty); // Cat@14ae5a5
    }
}


当通过System.out.println()时,参数为类对象。这时就会调用类对象的toString方法。

而类对象的toString方法返回的是对象的对象名+@+对象的十六进制的哈希值。