原型办法与享元办法

原型办法和享元办法,前者是在创立多个实例时,对创立进程的功能进行调优;后者是用削减创立实例的办法,来调优体系功能。这么看,你会不会觉得两个办法有点彼此矛盾呢?

其实不然,它们的运用是分场景的。在有些场景下,咱们需求重复创立多个实例,例如在循环体中赋值一个目标,此刻咱们就能够选用原型办法来优化目标的创立进程;而在有些场景下,咱们则能够防止重复创立多个实例,在内存中同享目标就好了。

今天咱们就来看看这两种办法的适用场景,看看怎么运用它们来进步体系功能。

原型办法

原型办法是经过给出一个原型目标来指明所创立的目标的类型,然后运用本身完成的克隆接口来仿制这个原型目标,该办法便是用这种办法来创立出更多同类型的目标。

运用这种办法创立新的目标的话,就无需再经过new实例化来创立目标了。这是由于Object类的clone办法是一个本地办法,它能够直接操作内存中的二进制流,所以功能相对new实例化来说,更佳。

完成原型办法

咱们现在经过一个简略的比如来完成一个原型办法:

   //完成Cloneable 接口的原型笼统类Prototype
   class Prototype implements Cloneable {
        //重写clone办法
        public Prototype clone(){
            Prototype prototype = null;
            try{
                prototype = (Prototype)super.clone();
            }catch(CloneNotSupportedException e){
                e.printStackTrace();
            }
            return prototype;
        }
    }
    //完成原型类
    class ConcretePrototype extends Prototype{
        public void show(){
            System.out.println("原型办法完成类");
        }
    }
    public class Client {
        public static void main(String[] args){
            ConcretePrototype cp = new ConcretePrototype();
            for(int i=0; i< 10; i++){
                ConcretePrototype clonecp = (ConcretePrototype)cp.clone();
                clonecp.show();
            }
        }
    }

要完成一个原型类,需求具有三个条件:

  • 完成Cloneable接口:Cloneable接口与序列化接口的效果相似,它只是告诉虚拟机能够安全地在完成了这个接口的类上运用clone办法。在JVM中,只要完成了Cloneable接口的类才能够被仿制,否则会抛出CloneNotSupportedException反常。
  • 重写Object类中的clone办法:在Java中,一切类的父类都是Object类,而Object类中有一个clone办法,效果是回来目标的一个仿制。
  • 在重写的clone办法中调用super.clone():默许情况下,类不具有仿制目标的才能,需求调用super.clone()来完成。

从上面咱们能够看出,原型办法的首要特征便是运用clone办法仿制一个目标。一般,有些人会误以为 Object a=new Object();Object b=a; 这种办法便是一种目标仿制的进程,但是这种仿制只是目标引证的仿制,也便是a和b目标指向了同一个内存地址,假如b修正了,a的值也就跟着被修正了。

咱们能够经过一个简略的比如来看看普通的目标仿制问题:

class Student {
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name= name;
    }
}
public class Test {
    public static void main(String args[]) {
        Student stu1 = new Student();
        stu1.setName("test1");
        Student stu2 = stu1;
        stu2.setName("test2");
        System.out.println("学生1:" + stu1.getName());
        System.out.println("学生2:" + stu2.getName());
    }
}

假如是仿制目标,此刻打印的日志应该为:

学生1:test1
学生2:test2

但是,实践上是:

学生1:test2
学生2:test2

经过clone办法仿制的目标才是真实的目标仿制,clone办法赋值的目标完全是一个独立的目标。刚刚讲过了,Object类的clone办法是一个本地办法,它直接操作内存中的二进制流,特别是仿制大目标时,功能的差别非常明显。咱们能够用 clone 办法再完成一遍以上比如。

//学生类完成Cloneable接口
class Student implements Cloneable{
    private String name;  //姓名
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name= name;
    }
   //重写clone办法
   public Student clone() {
        Student student = null;
        try {
            student = (Student) super.clone();
            } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            }
            return student;
   }
}
public class Test {
    public static void main(String args[]) {
        Student stu1 = new Student();  //创立学生1
        stu1.setName("test1");
        Student stu2 = stu1.clone();  //经过克隆创立学生2
        stu2.setName("test2");
        System.out.println("学生1:" + stu1.getName());
        System.out.println("学生2:" + stu2.getName());
    }
}

运转成果:

学生1:test1
学生2:test2

深仿制和浅仿制

在调用super.clone()办法之后,首先会检查当时目标所属的类是否支撑clone,也便是看该类是否完成了Cloneable接口。

假如支撑,则创立当时目标所属类的一个新目标,并对该目标进行初始化,使得新目标的成员变量的值与当时目标的成员变量的值如出一辙,但关于其它目标的引证以及List等类型的成员属性,则只能仿制这些目标的引证了。所以简略调用super.clone()这种克隆目标办法,便是一种浅仿制。

所以,当咱们在运用clone()办法完成目标的克隆时,就需求注意浅仿制带来的问题。咱们再经过一个比如来看看浅仿制。

//界说学生类
class Student implements Cloneable{
    private String name; //学生姓名
    private Teacher teacher; //界说教师类
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Teacher getTeacher() {
        return teacher;
    }
    public void setTeacher(Teacher teacher) {
        this.teacher = teacher;
    }
   //重写克隆办法
   public Student clone() {
        Student student = null;
        try {
            student = (Student) super.clone();
            } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            }
            return student;
   }
}
//界说教师类
class Teacher implements Cloneable{
    private String name;  //教师姓名
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name= name;
    }
   //重写克隆办法,对教师类进行克隆
   public Teacher clone() {
        Teacher teacher= null;
        try {
            teacher= (Teacher) super.clone();
            } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            }
            return student;
   }
}
public class Test {
    public static void main(String args[]) {
        Teacher teacher = new Teacher (); //界说教师1
        teacher.setName("刘教师");
        Student stu1 = new Student();  //界说学生1
        stu1.setName("test1");
        stu1.setTeacher(teacher);
        Student stu2 = stu1.clone(); //界说学生2
        stu2.setName("test2");
        stu2.getTeacher().setName("王教师");//修正教师
        System.out.println("学生" + stu1.getName + "的教师是:" + stu1.getTeacher().getName);
        System.out.println("学生" + stu1.getName + "的教师是:" + stu2.getTeacher().getName);
    }
}

运转成果:

学生test1的教师是:王教师
学生test2的教师是:王教师

调查以上运转成果,咱们能够发现:在咱们给学生2修正教师的时候,学生1的教师也跟着被修正了。这便是浅仿制带来的问题。

咱们能够经过深仿制来处理这种问题,其实深仿制便是基于浅仿制来递归完成具体的每个目标,代码如下:

   public Student clone() {
        Student student = null;
        try {
            student = (Student) super.clone();
            Teacher teacher = this.teacher.clone();//克隆teacher目标
            student.setTeacher(teacher);
            } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            }
            return student;
   }

适用场景

前面我详述了原型办法的完成原理,那究竟什么时候咱们要用它呢?

在一些重复创立目标的场景下,咱们就能够运用原型办法来进步目标的创立功能。例如,我在开头提到的,循环体内创立目标时,咱们就能够考虑用clone的办法来完成。

例如:

for(int i=0; i<list.size(); i++){
  Student stu = new Student();
  ...
}

咱们能够优化为:

Student stu = new Student();
for(int i=0; i<list.size(); i++){
 Student stu1 = (Student)stu.clone();
  ...
}

除此之外,原型办法在开源结构中的使用也非常广泛。例如Spring中,@Service默许都是单例的。用了私有全局变量,若不想影响下次注入或每次上下文获取bean,就需求用到原型办法,咱们能够经过以下注解来完成,@Scope(“prototype”)。

享元办法

享元办法是运用同享技能有效地最大极限地复用细粒度目标的一种办法。该办法中,以目标的信息状况划分,能够分为内部数据和外部数据。内部数据是目标能够同享出来的信息,这些信息不会随着体系的运转而改动;外部数据则是在不同运转时被标记了不同的值。

享元办法一般能够分为三个人物,分别为 Flyweight(笼统享元类)、ConcreteFlyweight(具体享元类)和 FlyweightFactory(享元工厂类)。笼统享元类一般是一个接口或笼统类,向外界供给享元目标的内部数据或外部数据;具体享元类是指具体完成内部数据同享的类;享元工厂类则是首要用于创立和管理享元目标的工厂类。

完成享元办法

咱们仍是经过一个简略的比如来完成一个享元办法:

//笼统享元类
interface Flyweight {
    //对外状况目标
    void operation(String name);
    //对内目标
    String getType();
}
//具体享元类
class ConcreteFlyweight implements Flyweight {
    private String type;
    public ConcreteFlyweight(String type) {
        this.type = type;
    }
    @Override
    public void operation(String name) {
        System.out.printf("[类型(内涵状况)] - [%s] - [名字(外在状况)] - [%s]\n", type, name);
    }
    @Override
    public String getType() {
        return type;
    }
}
//享元工厂类
class FlyweightFactory {
    private static final Map<String, Flyweight> FLYWEIGHT_MAP = new HashMap<>();//享元池,用来存储享元目标
    public static Flyweight getFlyweight(String type) {
        if (FLYWEIGHT_MAP.containsKey(type)) {//假如在享元池中存在目标,则直接获取
            return FLYWEIGHT_MAP.get(type);
        } else {//在呼应池不存在,则新创立目标,并放入到享元池
            ConcreteFlyweight flyweight = new ConcreteFlyweight(type);
            FLYWEIGHT_MAP.put(type, flyweight);
            return flyweight;
        }
    }
}
public class Client {
    public static void main(String[] args) {
        Flyweight fw0 = FlyweightFactory.getFlyweight("a");
        Flyweight fw1 = FlyweightFactory.getFlyweight("b");
        Flyweight fw2 = FlyweightFactory.getFlyweight("a");
        Flyweight fw3 = FlyweightFactory.getFlyweight("b");
        fw1.operation("abc");
        System.out.printf("[成果(目标比照)] - [%s]\n", fw0 == fw2);
        System.out.printf("[成果(内涵状况)] - [%s]\n", fw1.getType());
    }
}

输出成果:

[类型(内涵状况)] - [b] - [名字(外在状况)] - [abc]
[成果(目标比照)] - [true]
[成果(内涵状况)] - [b]

调查以上代码运转成果,咱们能够发现:假如目标已经存在于享元池中,则不会再创立该目标了,而是共用享元池中内部数据共同的目标。这样就削减了目标的创立,同时也节省了相同内部数据的目标所占用的内存空间。

适用场景

享元办法在实践开发中的使用也非常广泛。例如Java的String字符串,在一些字符串常量中,会同享常量池中字符串目标,然后削减重复创立相同值目标,占用内存空间。代码如下:

 String s1 = "hello";
 String s2 = "hello";
 System.out.println(s1==s2);//true

还有,在日常开发中的使用。例如,池化技能中的线程池便是享元办法的一种完成;将商品存储在使用服务的缓存中,那么每逢用户获取商品信息时,则不需求每次都从redis缓存或者数据库中获取商品信息,并在内存中重复创立商品信息了。

总结

原型办法和享元办法,在开源结构,和实践开发中,使用都非常广泛。

在不得已需求重复创立很多同一目标时,咱们能够运用原型办法,经过clone办法仿制目标,这种办法比用new和序列化创立目标的效率要高;在创立目标时,假如咱们能够共用目标的内部数据,那么经过享元办法同享相同的内部数据的目标,就能够削减目标的创立,完成体系调优。

本文由mdnice多平台发布