什么是单例
单例对象的类必须保证只有一个实力存在----这是维基百科上对单例的定义,这也可以作为对意图实现单例模式的代码进行检验的标准。
单例分为两大类
1.懒汉式:指全局的单例实例在第一次被使用时构建。
2.饿汉式:指全局的单例实例在类装载时构建。
平常我们使用较多的是懒汉式的单例
下面详细介绍一下两者的区别
1.懒汉式:
最简单的写法
//代码1
public class Single1 {
private static Single1 instance;
public static Single1 getInstance() {
if (instance == null) {
instance = new Single1();
}
return instance;
}
}
//代码1.1
public class Single1 {
private static Single1 instance;
//将构造方法变成私有的
private Single1() {}
public static Single1 getInstance() {
//判断是否已有实例对象
if (instance == null) {
instance = new Single1();
}
return instance;
}
}
这种方法在大多数情况下是没有问题的,但是,当进行多线撑工作时,如果同时运行到if(instance==null),都判断为空的情况下,就会同时创建凉的线程的示例,就不属于单例了。
synchronized版本
synchronized版本
为了修改上面的问题,我们添加一个同步锁,修改代码如下:
//代码2
public class Single2 {
private static Single2 instance;
private Single2() {}
public static synchronized Single2 getInstance() {
if (instance == null) {
instance = new Single2();
}
return instance;
}
}
我们在原来的基础上添加了一个synchronized关键字以后,getInstance方法就会锁上了,如果遇到了两个线程同时执行这个方法,就会有一个获得同步锁先执行方法,另一个则需要等大,第一个执行完之后,才会执行第二个。这样做就避免了可能出现因为多线程导致多个实例的情况。
但是这种方法也有一个问题:给gitInstance方法加锁,虽然会避免了可能会出现的多个实例问题,但是会强制除T1之外的所有线程等待,实际上会对程序的执行效率造成负面影响。
双重检查(Double-Check)版本
代码2相对于代码1的效率问题,其实是为了解决1%几率的问题,而使用了一个100%出现的防护盾。那有一个优化的思路,就是把100%出现的防护盾,也改为1%的几率出现,使之只出现在可能会导致多个实例出现的地方。
———我们使用下面的方法,来解决这个问题:
//代码3
public class Single3 {
private static Single3 instance;
private Single3() {}
public static Single3 getInstance() {
if (instance == null) {
synchronized (Single3.class) {
if (instance == null) {
instance = new Single3();
}
}
}
return instance;
}
}
这个方法相对来说比较复杂,其中出现了两次if(instancenull)的、判断,这个叫双重检查==
第一个if(instance==null)是为了解决代码2中的效率问题,只有instance为null的时候,才进入synchronized的代码块,大大的减少了机滤
第二个if(instance==null) ,是为了防止出现多个实例的情况。
为了是我们的代码看起来更加完美,我们又进行了一些处理,终极版本的代码如下:
终极版本:volatile
//代码4
public class Single4 {
private static volatile Single4 instance;
private Single4() {}
public static Single4 getInstance() {
if (instance == null) {
synchronized (Single4.class) {
if (instance == null) {
instance = new Single4();
}
}
}
return instance;
}
}
volatile关键字的作用是禁止指令重排(在不影响最终结果的情况下,可能会对一些语句的执行顺序进行调整。),把instance声明为volatile之后,对它的写操作就会有一个内存屏障,这样,在它的赋值完成之前,就不用会调用读操作。
注意:volatile阻止的不是singleton = new Singleton()这句话内部的指令重排,而是保证了在一个操作完成之前,不会调用读操作(if (instance == null))。
2.饿汉式:
由于类装载的过程是由类加载器来执行的,这个过程也是由JVM来保证同步的,所以这种方式先天就有一个优势——能够免疫许多由多线程引起的问题。
实现方法如下:
public class SingleB {
private static final SingleB INSTANCE = new SingleB();
private SingleB() {}
public static SingleB getInstance() {
return INSTANCE;
}
}
对于饿汉式的单例,这个代码可以说是完美了,所以它出现的问题就是饿汉式单例本身的问题了——由于INSTANCE的初始化是在类加载时进行的,而类的加载是由ClassLoader来做的,所以开发者本来对于它初始化的时机就很难去准确把握。
知识点:什么时候是类装载时?
- new一个对象时
- 使用反射创建它的实例时
- 子类被加载时,如果父类还没被加载,就先加载父类
- jvm启动时执行的主类会首先被加载
- 一些其他的实现方式
以上五种方法会触发类被加载。
Effective Java1 静态内部类
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
这种写法对于内部类SingletonHolder,它是一个饿汉式的单例实现,在SingletonHolder初始化的时候会由ClassLoader来保证同步,使INSTANCE是一个真·单例。同时,由于SingletonHolder是一个内部类,只在外部类的Singleton的==getInstance()中被使用,所以它被加载的时机也就是在getInstance()==方法第一次被调用的时候。
Effective Java 2 枚举
public enum SingleInstance {
INSTANCE;
public void fun1() {
// do something
}
}
这种方法使代码更加简洁,而且还解决的了大部分的问题,想法非常优秀。但是在继承的场景中,就不是很适用了。
-----------------------------------------------------------------------分割线----------------------------------------------------------------------------------
继承
**语法:**修饰符 SubClass extends SuperClass{
//类定义部分
}
继承通过extends关键字来实现,其中的SunClass称为子类,SuperClass称为父类,
基类或超类,修饰符如果是public,则该类在整个项目中可见,若无public修饰符,则该类只在当前包课件,不可以使用private和protected修饰符。
抽象出的Pet类如下所示:
/**
* 宠物类,狗狗和企鹅的父类
*
*/
public class Pet {
private String name="无名氏";//昵称
private int health=100;//健康值
private int love=0;//亲密度
/**
* 无参构造方法
* @param name2
*/
public Pet(){
this.health=95;
System.out.println("执行宠物的无参构造方法");
}
/**
* 有参构造方法
*/
public Pet(String name){
this.name=name;
}
public String getName() {
return name;
}
public int getHealth() {
return health;
}
public int getLove() {
return love;
}
/**
* 输出宠物信息
*/
public void print(){
System.out.println("宠物的自白:\n我的名字叫"+this.name+",健康值是"+this.health+",和主人的亲密度是"+this.love+"。");
}
}
Dog类继承Pet类,代码如下所示:
/**
* 狗狗类,宠物的子类
*/
public class Dog extends Pet{
private String strain;//品种
/**
* 有参构造方法
*/
public Dog(String name,String strain) {
super(name);//此处不能用this.name=name
this.strain = strain;
}
public String getStrain() {
return strain;
}
Penguin类继承Pet,代码如下所示:
/**
* 企鹅类,宠物的子类
*/
public class Penguin extends Pet{
private String sex;//品种
/**
* 有参构造方法
*/
public Penguin(String name,String sex) {
super(name);//此处不能用this.name=name
this.sex = sex;
}
public String getSex() {
return sex;
}
编写测试类如下:
public class TestPet {
public static void main(String[] args) {
//1.创建宠物对象pet并输出信息
Pet pet=new Pet("贝贝");
pet.print();
//创建狗狗对象dog并输出信息
Dog dog=new Dog("欧欧","雪纳瑞");
dog.print();
//创建企鹅对象pgn并输出信息
Penguin pgn=new Penguin("楠楠","Q妹");
pgn.print();
}
}
运行结果如下:
什么是继承
继承是面向对象的三大特征之一,是java中实现代码重用的重要手段之一。java中只支持单继承,即每个类只能有一个直接父类。继承表达的是is a的关系,或者说是一种特殊和一般的关系,如Dog is a Pet。同样,可以让学生继承人,让苹果继承水果,让三角形集成几何图形。
在java中,子类可以从父类中继承到那些“财产”呢?
1.继承public和protected修饰的属性和方法,无论子类和父类是否在同一个包里。
2.继承默认权限修饰符修饰的属性和方法,但子类和父类必须在同一个包里。
3.无法继承private修饰的属性和方法。
4.无法继承父类的构造方法。
下面总结一下访问修饰符的访问权限
重写和继承关系中的构造方法
Dog类中重写父类的print()方法,代码如下:
/**
* 狗狗类,宠物的子类
*/
public class Dog extends Pet{
private String strain;//品种
/**
* 有参构造方法
*/
public Dog(String name,String strain) {
super(name);//此处不能用this.name=name
this.strain = strain;
}
public String getStrain() {
return strain;
}
/**
* 重写父类的print()方法
*/
public void print(){
super.print();//调用父类的print()方法
System.out.println("我是一只"+this.strain+"。");
}
}
Penguin类中重写父类的print()方法,代码如下:
/**
* 企鹅类,宠物的子类
*/
public class Penguin extends Pet{
private String sex;//品种
/**
* 有参构造方法
*/
public Penguin(String name,String sex) {
super(name);//此处不能用this.name=name
this.sex = sex;
}
public String getSex() {
return sex;
}
/**
* 重写父类的print()方法
*/
public void print(){
super.print();//调用父类的print()方法
System.out.println("性别是"+this.sex+"。");
}
}
运行结果如下:
方法重写要满足的条件:
1.重写方法和被重写方法必须具有相同的方法名。
2.重写方法和被重写方法必须具有相同的参数列表。
3.重写方法的返回值类型和被重写方法的返回值类型相同或者是其子类。
4.重写方法不能缩小被重写方法的访问权限。
重载和重写有什么区别和联系
1.重载涉及同一个类中的同名方法,要求方法名相同,参数列表不同。与返回值类型,访问修饰符无关。
重写设计的是子类和父类之间的同名方法,要求方法名相同,参数列表相同,返回值类型相同(或是其子类),访问修饰符不能严于父类。
如果在子类中想要调用父类的被重写的方法,应该怎样实现呢?
在子类方法中通过“super方法名”实现
super必须出现在子类(子类的方法和构造方法)中,而不是其他位置。
可以访问父类的成员,如父类的属性,方法,构造方法。
注意访问权限的限制,如无法通过super访问private成员。