【第11天】Java的单例模式、接口以及Object类常用的方法

版权声明:©2018 本文為博主來秋原創,轉載請註明出處,博主保留追責權利。 https://blog.csdn.net/qq_30257081/article/details/84201912

1 单例模式

  • 什么是设计模式?
    一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。世上本没有设计模式,用的人多了,也便有了设计模式。

  • 什么是单例模式?
    用来控制一个类有且只有一个对象的设计模式。

1.1 醉汉式

  • 实现的注意事项:
    • 私有化构造方法,以防止类体之外别人随意创建对象,通过new Moon获取到。
    • 创建一个私有的、静态的、属于本类类型的对象。
    • 提供一个公共的、静态的、返回本类类型对象的方法。
public class Test{
	public static void main(String[] args){
		
		//验证:
		Moon x = Moon.getMm();
		Moon y = Moon.getMm();
		Moon z = Moon.getMm();
		System.out.println(x == y);
		System.out.println(y == z);

	}
}

//只有一个月亮
class Moon{

	//私有化构造方法
	//private:私有化构造方法,防止类体之外别人随意new Moon对象获取到
	private Moon(){}

	//private:防止类体之外别人随意的对静态属性赋值,若设置为“Moon.mm = null;”就尴尬了
	//static:防止死循环*   Moon-> mm -> mm -> mm......
	private static Moon mm = new Moon();

	//getter
	//public:封装
	//static: 防止方法依赖于对象,只有类才可以定义,
	public static Moon getMm(){//月亮.getMm();
		return mm;
	}

}
  • 为何要“static Moon mm = new Moon();”静态化mm对象?

    • 类似
    class Student{
    	int age = 20;
    }
    

    在这里插入图片描述

  • 这样会造成如图的死循环,所以一定要使用static让每次调用都共享一个成员moon。

    class Moon{
    	Moon moon = new Moon();
    }
    

在这里插入图片描述

1.2 懒汉式

       因为还没有学习到线程控制,所以这里的懒汉式是不完善的,学习线程后再补全这里的内容。

class Moon{

	private Moon(){}

	private static Moon only;

	public static Moon getOnly(){
		//当被调用时对象是否存在,如果不存在,new一个返回,如果存在则直接返回
		//如果这样写,调用时可能会出现线程并发问题,需要继续完善
		if(only == null){
			only = new Moon();
			return only;
		}else{
			return only;
		}
	}
}

1.3 两者之间的区别

醉汉式不管用不用,总是先实例化,可能会浪费内存,但是只要调用就很快返回这个实例。效率较高,但内存空间占用较大。
懒汉式注重内存管理,节省内存空间,如果不调用就不创建。效率较低,但内存空间占用较小。

2 接口(interface)

       相当于工业生产中的规范。它是Java四大类*中的第二大类型,与abstract抽象类一样,它不能创建对象

  • Java四大类(是编译之后生成对应的.class文件的那种,注意与数据类型作区分)

    • 类(class)
    • 接口(interface)
    • 枚举(enum)
    • 注解(@interface)
  • interface如何定义?

interface XXX{
	//属性:
	//接口里面的属性默认加上三个修饰符:
	//public static final
	int x = 45;
	String y = "etoak";
	
	//方法:返回类型 方法签名();
	//接口里面的方法默认加上两个修饰符:
	//public abstract
	void test();
	int show();
}

类实现接口之后,值不需要修改了,方法需要覆盖(重写)后再调用执行。

  • 例:
public class TestInterface{
	public static void main(String[] args){

		//一个USB设备接入电脑
		USBKeyboard df = new USBKeyboard();
		Computer dell = new Computer();

		dell.open(df);
	}
}

/**
	USB设备和电脑之间的关系:

	先制定所有USB设备都需要尊重的规范/条约 -> 接口 interface

	找到一个类型的开发满足这样的规范 -> USB鼠标  USB键盘
	找到一个类型的开发使用这样的规范 -> 电脑

*/
//指定所有USB设备的规范/条约
interface USB{

	//接口里面定义属性和方法

	//标定电压
	//接口里面的属性默认加上三个修饰符:
	//public static final
	int v = 5;

	//连接电脑的功能
	//接口里面的方法默认加上两个修饰符:
	//public abstract
	void connect();
}
class USBKeyboard implements USB{

	@Override
	public void connect(){
		System.out.println("USB键盘连接电脑的方法");
	}
}

//类 implements 实现/遵循 接口规范
class USBMouse implements USB{

	//当我们拿着一个类去实现一个接口的时候 需要给出接口
	//里面所有抽象方法的具体实现

	@Override
	public void connect(){
		System.out.println("USB鼠标连接电脑的方法");
	}
}

class Computer{
	public void open(USB x){
		x.connect();
		System.out.println("电脑打开连接");
	}
}
  • 面试题

1 类与接口两两之间的关系:
类与类之间:继承关系(extends)
接口与接口:继承关系(extends)
类与接口:实现关系(implements)
另外

Java中的类只允许单根继承
Java中的接口允许多重实现
也就是说Java中的类在继承一个类的同时可以实现多个接口

2 从哪个版本开始,方法覆盖的时候允许加@Override注解?

类和类之间的方法覆盖:JDK5.0及以上
类和接口之间的方法覆盖:JDK6.0及以上

4:接口和抽象类之间的区别?★

名称 两者之间的关系 单根继承/多重实现 类型 对属性的定义 对方法的定义
接口 类implements(实现)接口 多重实现(一个类实现多个接口) interface 静态的最终变量(public static final) 抽象方法(public abstract)
抽象类 类extends抽象类 单根继承(只允许单继承) class 普通属性 抽象方法 + 普通方法

接口是对事物动作的抽象,而抽象类是对事物根源的抽象。 动作的抽象可以被不同事物的抽象共享,而事物的抽象属性只能对一类事物使用,比如飞机和鸟都会飞,飞这个动作就可以抽象为一个接口的方法供飞机和鸟实现,只是实现的内容不同。但是飞机和鸟自身特有的属性要定义在抽象类中。

关于接口和抽象类的区别理解,我找到了一篇能很全面并形象描述的文章,出自CSDN@chenssy 大神。

3 Object类常用的方法

       Object(对象)类,是所有引用数据类型的直接父类或者间接父类(父类的父类)。每个类直接或者间接继承了Object类,也就是说每个类创建之后默认都有自己的如下几个方法。定义返回类型为Object的方法,如果使用其他引用类型接收,需要强转。

  • 存在一个权限较小但功能较好的方法,如何将其权限改大:
public class Test{
	public static void main(String[] args){

	}
}

class A{
	protected void test(){
		System.out.println("优秀的方法实现");
	}
}

class B extends A{

	//重写将其访问权限改大
	@Override
	public void test(){
		//直接使用super调用父类的test()
		super.test();
	}
}

3.1 clone()

       使用Object类中clone()克隆出来的数组、对象虽然内容与之前相同,但地址发生改变。

public class TestClone{//extends Object
	public static void main(String[] args){

		Sheep s1 = new Sheep("克隆羊母体");
		Sheep s2 = s1.clone();
		System.out.println(s1 == s2);
	}
}
class Sheep{//extends Object

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

上面的代码报“错误: clone()可以在Object中访问protected/不兼容的类型”,是因为clone()没有显式重写前,默认执行的是Object类中的clone()方法。

查看Object类源码可得“protected native Object clone() throws CloneNotSupportedException”,protected意为可以在包(目录)内或者包外的具有继承关系的子类中使用,然而这里是在TestClone类中访问了Sheep类中的clone()方法,报错。如果是在Sheep类调用Sheep类的clone()则是可以的。

结合前叙中改大权限的办法,我们可以在Sheep类中将clone()方法重写,使其权限增大,这样就可以在TestClone类中访问了。

改后:

public class TestClone{//extends Object
	public static void main(String[] args) throws Exception{

		Sheep s1 = new Sheep("克隆羊母体");
		//调用的.clone()方法需要抛出异常,在方法上抛出
		Sheep s2 = s1.clone();
		System.out.println(s1 == s2);//--->false
	}
}
//如果类能够克隆,它需要满足某个条约(实现Cloneable接口)
class Sheep implements Cloneable{//extends Object

	String name;
	public Sheep(String name){
		this.name = name;
	}
	
	//这里的返回类型使用了协变返回类型:任何Object类的子类都所以可以作为返回类型
	//重写Object类中的clone()方法,可以改大访问权限,在TestClone类中访问到
	//因为是super.直接调用了父类的clone(),所以异常抛出需与父类相同或更小
	@Override
	public Sheep clone() throws CloneNotSupportedException{

		//在子类调用父类的clone()帮助我们去克隆一个对象,与父类相同
		Object obj = super.clone();
		//返回子类Sheep类型的obj
		return (Sheep)obj;
	}
}

所以要使得类Sheep可以在TestClone类中调用其自身clone()方法,需要:

  • 重写父类Object的clone()
  • 在Sheep类实现Cloneable接口
  • 在TestClone这个需要访问clone()方法的类抛出一个异常

3.2 finalize()

       对象的“遗言”方法,当一个对象要被gc线程回收时,会主动调用这个对象。

public class TestFinalize{
	public static void main(String[] arfgs){

		while(true){
			Teacher tea = new Teacher();
			//主动召唤gc线程进来回收
			//System.gc();
		}
	}
}

class Teacher{//extends Object

	public Teacher(){
		System.out.println("创建Teacher的对象");
	}

	//内存快要满了时,GC出来工作回收这个对象之前,执行这个方法
	@Override
	public void finalize(){
		System.out.println("快要被GC回收了=======================");
	}
}
  • 如何主动召唤gc线程进来回收? 使用“System.gc();”

3.3 toString() (需重写)

       指定一个对象打印显示的内容,当要打印一个引用类型的对象时,底层都会使用这个对象调用toString()。

  • 在toString()没有覆盖(直接调用由Object类继承来的方法)时打印:类型@XXX。如下例
public class TestToString1{
	public static void main(String[] args){

		String str = new String("张三");
		System.out.println(str);//--->张三
		System.out.println(str.toString());//--->张三
	
		Student stu = new Student("张三");
		System.out.println(stu);//--->Student@5a20d10a
		System.out.println(stu.toString());//--->Student@5a20d10a
	}
}

class Student{

	String name;

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

为何String的toString()、直接输出可以获得“张三”,而自定义的引用数据类型输出的是地址?
       因为String类重写了父类Object类中的toString()方法,这样在输出其对象引用时,直接输出其toString()方法返回的内容。Student类中未重写该方法。

       如果我们想要得到一条有用的信息时,需要在我们定义的Object子类中重写toString()。

public class TestToString2{
	public static void main(String[] args){

		String str = new String("张三");
		System.out.println(str);//--->张三
		System.out.println(str.toString());//--->张三
	
		Student stu = new Student("张三");
		System.out.println(stu);//--->张三
		System.out.println(stu.toString());//--->张三
	}
}

class Student{

	String name;

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

	//重写了Object类中的toString()方法
	@Override
	public String toString(){
		return name;
	}
}

例题:

//使用toString()打印出有用的信息
public class ExampleToString{
	public static void main(String[] args){

		Person x = new Person("张三",33,'男');
		System.out.println(x);

		Person y = new Person("李四",28,'女');
		System.out.println(y);
	}
}

class Person{
	String name;
	int age;
	char gender;

	public Person(String name,int age,char gender){
		this.name = name;
		this.age = age;
		this.gender = gender;
	}

	@Override
	public String toString(){
		return name + (gender == '男'? "先生":"女士" ) + "今年" + age + "岁";
	}
}

3.4 equals()(需重写)

       制定一个类型的比较规则,当我们想要将内存里面不同的两个对象视为“逻辑”相等对象的时候,需要重写equals方法,来定义我们自己的相等法则。
       在equals()没有被覆盖时,比较两个对象地址。如下例

public class TestEquals1{
	public static void main(String[] args){

		String x = new String("OK");
		String y = new String("OK");

		System.out.println(x == y);//判断地址是否相同--->false
		System.out.println(x.equals(y));//判断内容是否相同--->true

		Student a = new Student("张三");
		Student b = new Student("张三");

		System.out.println(a == b);//判断地址是否相同--->false
		System.out.println(a.equals(b));//判断地址是否相同--->false
	}
}

class Student{//extends Object
	String name;
	public Student(String name){
		this.name = name;
	}
}

       Student类中重写equals(),使Student的引用调用equals()时判断名字是否相同。也可以传入更多的值,判断多个条件符合来返回equals()方法的结果(逻辑相等),此处只是举一个例子。

public class TestEquals2{
	public static void main(String[] args){

		String x = new String("OK");
		String y = new String("OK");

		System.out.println(x == y);//判断地址是否相同--->false
		System.out.println(x.equals(y));//判断内容是否相同--->true

		Student a = new Student("张三");
		Student b = new Student("张三");

		System.out.println(a == b);//判断地址是否相同--->false
		System.out.println(a.equals(b));//判断内容是否相同--->true
	}
}

class Student{//extends Object
	String name;
	public Student(String name){
		this.name = name;
	}
	
	//重写:只要两个学生的姓名一样就视为相等对象
	@Override
	public boolean equals(Object obj){//stu1.equals(stu2)

		//先找到要比较的两个学生对象
		Student s1 = this;//当前调用该方法的对象
		Student s2 = (Student)obj;//要比较的第二个学生对象

		//分别通过对象得到他们的姓名
		String x = s1.name;
		String y = s2.name;
		
		//使用String的equals(),判断学生的姓名内容是否相同
		return x.equals(y);
	}
}

但上面这样重写的方式并不是特别健壮和高效,需要注意添加一些条件判断以备出现的不健壮情况,并提升效率。

public class TestEquals3{
	public static void main(String[] args){

		String x = new String("OK");
		String y = new String("OK");

		System.out.println(x == y);//判断地址是否相同--->false
		System.out.println(x.equals(y));//判断内容是否相同--->true

		Student a = new Student("张三");
		Student b = new Student("张三");

		System.out.println(a == b);//判断地址是否相同--->false
		System.out.println(a.equals(b));//判断内容是否相同--->true
	}
}

class Student{//extends Object
	String name;
	public Student(String name){
		this.name = name;
	}
	
	//重写:只要两个学生的姓名一样就视为相等对象
	@Override
	public boolean equals(Object obj){

		//对参数obj排空判断
		//参数是Object类,任何一个引用数据类型默认值都是null
		//为了保证代码健壮,此处不考虑调用对象的引用为空的情况,只考虑参数列表内的情况
		if(obj == null) return false;

		//由于参数是Object类型,导致所有的引用数据类型都可以传进来 
		//但学生对象就应该和学生类的对象比较,instanceof的意思是判断obj是否是Student类的对象
		//为了保证代码健壮
		if(!(obj instanceof Student)) return false;
		
		//为了保证程序更加高效
		if(this == obj) return true;
		
		//先找到要比较的两个学生对象
		Student s1 = this;//当前调用该方法的对象
		Student s2 = (Student)obj;//要比较的第二个学生对象

		//分别通过对象得到他们的姓名
		String x = s1.name;
		String y = s2.name;
		
		//使用String的equals(),判断学生的姓名内容是否相同
		return x.equals(y);
	}
}

最后总结得,重写equals(),需在开始时添加几个判断:

	@Override
	public boolean equals(Object obj){
		//防止参数列表传null
		//健壮性
		if(obj == null)return false;
		//防止参数列表不传空但是乱传,这里对于传入的不属于此类的对象均返回false
		//防止类造型异常classCastException
		//健壮性
		if(!(obj instanceof 类型))return false;
		//对于传入的对象和对象引用完全一致,后面的都不用判断了,都对
		//效率
		if(obj == this)return true;
			return xxx.equals(yyy);
	}
  • "= ="和equals()之间的区别?
    1 “= =”:是一个运算符;
    equals():是Object类里面的一个方法。
    2 “= =”:比较左右两边的值是否相等,等号两边是基本数据类型,比较数值;如果是引用数据类型则比较地址。
    equals():是制定一个类型的比较规则,只能比较引用数据类型。程序员可以按照自己的意愿对equals()进行覆盖,当equals方法没有覆盖的时候,比较两个对象的地址。

3.5 hashCode()(需重写)

       制定一个对象的散列特征码。散列的意义是将一大组数据分散为不同的小组。散列特征码就是对象去往哪个小组的依据。有图例:
在这里插入图片描述

  • hashCode()的用途主要用于后面的HashSet/HashMap,它的底层实现为哈希表,它类似上面的学生分类,分为了16组。元素去往哪个小组,根据hashCode()方法返回的值模以分组个数来决定。

  • 开发文档中写到当equals()被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码,即在这种情况下如果x.equals(y)为true,那么x.hashCode() == y.hashCode()。

  • 但如果equals()方法被重写但hashCode()不被重写,会出现如下状况:

public class TestHashCode1{
	public static void main(String[] args){

		String x = new String("OK");
		String y = new String("OK");
		System.out.println(x.equals(y));//--->true
		System.out.println(x.hashCode());//--->2524
		System.out.println(y.hashCode());//--->2524


		Student a = new Student("张三");
		Student b = new Student("张三");
		System.out.println(a.equals(b));//--->true equals()被重写,两个对象内容相同
		//虽然已经重写了equals()但hashCode()依然为继承得到的在Object类中定义的
		System.out.println(a.hashCode());//--->846725353
		System.out.println(b.hashCode());//--->1686362849
	}
}

class Student{//extends Object -> hashCode()
	String name;

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

	@Override
	public boolean equals(Object obj){
		if(obj == null) return false;
		if(!(obj instanceof Student)) return false;
		if(this == obj) return true;
		return this.name.equals(((Student)obj).name);
	}
}

所以,重写equals()之后,一定要再重写hashCode()。

public class TestHashCode2{
	public static void main(String[] args){

		String x = new String("OK");
		String y = new String("OK");
		System.out.println(x.equals(y));//String extends Object -> equals() + hashCode()
		System.out.println(x.hashCode());
		System.out.println(y.hashCode());


		Student a = new Student("张三");
		Student b = new Student("张三");
		System.out.println(a.equals(b));
		System.out.println(a.hashCode());
		System.out.println(b.hashCode());
	}
}

class Student{//extends Object -> hashCode()
	String name;

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

	@Override
	public boolean equals(Object obj){
		if(obj == null) return false;
		if(!(obj instanceof Student)) return false;
		if(this == obj) return true;
		return this.name.equals(((Student)obj).name);
	}
	//重写hashCode()
	@Override
	public int hashCode(){
		return name.hashCode();
	}
}

另外要防止hashCode()返回的数值出现equals()不相等,但编码出现了重复,这多是因为要转换hashCode()时出现两个对象原本需要拼接的字符串值均不相同,拼起来之后相同了。

3.6 综合应用

/**
	定义一个Computer类型:
		有的属性:name  size(尺寸)  color(颜色)  isSoldOut(是否售罄)

		创建对象的时候对所有的属性的赋值

		打印电脑类对象的时候 显示:
			XXX牌子的电脑新上市一款XXX颜色,尺寸是XXX,已售罄/未售罄。

		toString()使用:
			StringBuffer buffer = new StringBuffer();
			buffer.append(name);
			buffer.append("牌子的电脑");

		只要两个电脑所有属性都一样 那么视为相等对象

		只要两个对象视为相等对象 哈希码值一样
*/
public class Exec{
	public static void main(String[] args){
		Computer c1 = new Computer("联想", 15.0, '黑', true);
		Computer c2 = new Computer("联想", 15.0, '黑', true);
		System.out.println("equals():" + c1.equals(c2));
		System.out.println("c1.hashCode():" + c1.hashCode());
		System.out.println("c2.hashCode():" + c2.hashCode());
		System.out.println("c1:" + c1);
		System.out.println("c2:" + c2);
	}
}

class Computer{
	//品牌
	String name;
	//尺寸
	double size;
	//颜色
	char color;
	//是否已售罄
	boolean isSoldOut;

	public Computer(String name, double size, char color, boolean isSoldOut){
		this.name = name;
		this.size = size;
		this.color = color;
		this.isSoldOut = isSoldOut;
	}

	@Override
	public String toString(){
		StringBuffer sb = new StringBuffer(String.valueOf(name));
		//转换为StringBuffer的目的是提高拼串的效率,所以不要再在append中写"+"拼串了
		sb.append("品牌的电脑新上市一款");
		sb.append(String.valueOf(color));
		sb.append("色,尺寸是");
		sb.append(String.valueOf(size));
		sb.append(isSoldOut ? ",已售罄。" : ",未售罄。");
		//StringBuffer转换为String
		//方法1
		//return sb + "";
		//方法2(推荐,StringBuffer中也重写了toString()方法)
		return sb.toString();
		//方法3 return String.valueOf(sb)
	}

	@Override
	public boolean equals(Object obj){
		if(obj == null) return false;
		if(!(obj instanceof Computer)) return false;
		if(this == obj) return true;
		return name.equals(((Computer)obj).name) && size == ((Computer)obj).size && color == ((Computer)obj).color && isSoldOut == ((Computer)obj).isSoldOut;

	}
	//这里String先转为哈希码,不要整个拼起来之后再转,防止哈希重码
	//比如"张3" + "13" 和 "张" + "313" 拼串就会造成重码		
	@Override
	public int hashCode(){
		return name.hashCode() + (int)size + color + (isSoldOut ? 1 : 0);
	}
}

猜你喜欢

转载自blog.csdn.net/qq_30257081/article/details/84201912