1 单例模式
-
什么是设计模式?
一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。世上本没有设计模式,用的人多了,也便有了设计模式。 -
什么是单例模式?
用来控制一个类有且只有一个对象的设计模式。
1.1 醉汉式(预加载)
- 实现的注意事项:
- 私有化构造方法,以防止类体之外别人随意创建对象,通过new Moon获取到。
- 创建一个私有的、静态的、属于本类类型的对象。
- 提供一个公共的、静态的、返回本类类型对象的方法。
public class TestSingleton{
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 懒汉式(懒加载)
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 | 普通属性 | 抽象方法 + 普通方法 |
接口是对事物动作的抽象,而抽象类是对事物根源的抽象。 动作的抽象可以被不同事物的抽象共享,而事物的抽象属性只能对一类事物使用,比如飞机和鸟都会飞,飞这个动作就可以抽象为一个接口的方法供飞机和鸟实现,只是实现的内容不同。但是飞机和鸟自身特有的属性要定义在抽象类中。
关于接口和抽象类的区别理解,我找到了一篇能很全面并形象描述的文章,出自优快云@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);
}
}