zoukankan      html  css  js  c++  java
  • Java 不可变对象

      不可变对象(immutable objects):一旦对象被创建,它们的状态就不能被改变(包括基本数据类型的值不能改变,引用类型的变量不能指向其他的对象,引用类型指向的对象的状态也不能改变),每次对他们的改变都是产生了新的对象。JDK本身就自带了immutable类,比如String,Integer以及其他包装类。

    遵循原则:

    1. 类添加final修饰符,保证类不被继承
    如果类可以被继承会破坏类的不可变性机制,只要继承类覆盖父类的方法并且继承类可以改变成员变量值,那么一旦子类以父类的形式出现时,不能保证当前类是否可变。

    2. 保证所有成员变量必须私有,并且加上final修饰
    通过这种方式保证成员变量不可改变。但只做到这一步还不够,因为如果是对象成员变量有可能再外部改变其值。所以第4点弥补这个不足。

    3. 不提供改变成员变量的方法,包括setter
    避免通过其他接口改变成员变量的值,破坏不可变特性。

    4.通过构造器初始化所有成员,进行深拷贝(deep copy)

    如果构造器传入的对象直接赋值给成员变量,还是可以通过对传入对象的修改进而导致改变内部变量的值。例如:

    public final class ImmutableDemo {  
        private final int[] myArray;  
        public ImmutableDemo(int[] array) {  
         // this.myArray = array;  wrong  
            this.myArray = array.clone(); // 采用深度copy来创建一个新的对象保证不会通过传入的array来修改myArray的数组元素  
        }  
    }

    5. 在getter方法中,不要直接返回对象本身,而是克隆对象,并返回对象的拷贝
    这种做法也是防止对象外泄,防止通过getter获得内部可变成员对象后对成员变量直接操作,导致成员变量发生改变。

    优点:

    1. Immutable对象是线程安全的,可以不用被synchronize就在并发环境中共享
    2. Immutable对象简化了程序开发,因为它无需使用额外的锁机制就可以在线程间共享
    3. Immutable对象提高了程序的性能,因为它减少了synchroinzed的使用
    4. Immutable对象是可以被重复使用的,你可以将它们缓存起来重复使用,就像字符串字面量和整型数字一样。你可以使用静态工厂方法来提供类似于valueOf()这样的方法,它可以从缓存中返回一个已经存在的Immutable对象,而不是重新创建一个。

    缺点:

      由于不可变对象不能修改重用,会制造大量垃圾,字符串就是一个典型的例子,但合理的使用immutable对象会创造很大的价值。

    String不可变类

      Java的String类是不可变对象(immutable object),即创建后不可以改变的对象。一旦一个string对象在内存(堆)中被创建出来,他就无法被修改。特别要注意的是,String类的所有方法都没有改变字符串本身的值,都是返回了一个新的对象。如果你需要频繁地修改一个字符串对象,可以使用StringBuffer或者StringBuilder,否则将会浪费大量时间进行垃圾回收,因为每次都会创建一个新的字符串。

      String的不可变特性主要为了满足常量池、线程安全、类加载的需求。

    String s = "abcd";  // 可以修改变量s的引用,因为s不是final类型的变量(初始化之后不能更改),但是s指向的堆内存中的对象是不能更改的,因为它的类型是不可变的String类
    String s2 = s;      // s2保存了和s相同的引用值,他们指向同一个对象。
    s = s.concat("ef"); // 重新创建一个string对象的引用
    s.toUpperCase();    // 此处并没有改变“abcd“的值,而是创建了一个新的String类“ABCD”,然后将新的实例的指向变量s

    相对于可变对象,String作为不可变对象有很多优势:

    1) 不可变对象可以提高String Pool的效率和安全性。如果一个对象是不可变的,那么拷贝该对象的内容时,只需复制地址而不用复制它本身,需要很小的内存效率也很高。

    2) 不可变对象对于多线程是安全的,因为在多线程的情况下,一个可变对象的值(堆中的实例)很可能被其他进程改变,这样会造成不可预期的结果,而使用不可变对象就可以避免这种情况。

    3) String被许多的Java类(库)用来当做参数,例如 网络连接地址URL,文件路径path,还有反射机制所需要的String参数等, 假若String不是固定不变的,可能被黑客们改变字符串变量指向的对象的值,从而引起各种安全隐患。

    4) 只有当字符串是不可变的,字符串池才有可能实现。字符串池的实现可以在运行时节约很多heap空间,因为不同的字符串变量都指向池中的同一个字符串。但如果字符串是可变的,如果变量改变了它的对象的内容,那么其它指向这个对象的变量的值也会一起改变。

    5) 因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。

    class User {
        String name;
        public User(String name) { this.name = name; }
        public void setName(String name) { this.name = name; }
        public String toString() { return name; }
    }
    
    class UserWithHashCode extends User{
        public UserWithHashCode(String name) { super(name); }
        public int hashCode() {
            final int prime = 31; int result = 1;
            result = prime * result + ((name == null) ? 0 : name.hashCode());
            return result;
        }
        public boolean equals(Object obj) {
            if (this == obj) return true;
            if (obj == null) return false;
            if (getClass() != obj.getClass()) return false;
            User other = (User) obj;
            if (name == null) { if (other.name != null) return false; } else if (!name.equals(other.name)) return false;
            return true;
        }
    }
    public class Test {
        public static void main(String[] args) {
            // HashMap put和get方法都用key的hashcode去判断是否为同一个对象,自定义类默认hashcode为对象的地址。
            // String类的hashcode是根据字符串的值来计算的,所以值相同的字符串hashcode也一样。
            Map<String, Integer> map1 = new HashMap<String, Integer>(); 
            String str = "key1";
            map1.put(str, 1);            print(map1, str);  // Map: {key1=1}, value of key:1    
            str = "key2";                print(map1, str);  // Map: {key1=1}, value of key:null  变量s指向新的对象
            str = new String("key1");    print(map1, str);  // Map: {key1=1}, value of key:1     变量s指向原来的对象,map里的key不会改变。
            
            Map<User, Integer> map2 = new HashMap<User, Integer>();
            User user = new User("Mike");
            map2.put(user, 1);     print(map2, user);             // Map: {Mike=1}, value of key:1
            user.setName("Sara");  print(map2, user);             // Map: {Sara=1}, value of key:1    user指向的对象保持不变
            user.setName("Mike");  print(map2, new User("Mike")); // Map: {Mike=1}, value of key:null 此处使用新的对象,因此取不到值
            
            Map<UserWithHashCode, Integer> map3 = new HashMap<UserWithHashCode, Integer>();
            map3.put(new UserWithHashCode("lily"), 1);
            print(map3, new UserWithHashCode("Mike")); // Map: {lily=1}, value of key:null 
            print(map3, new UserWithHashCode("lily")); // Map: {lily=1}, value of key:1   重写hashCode和equals方法, 使得name相同的对象相等
        }        
    }

     Stringl类的源码:

    public final class String  
        implements java.io.Serializable, Comparable<String>, CharSequence  
    {  
        /** The value is used for character storage. */  
        private final char value[];  
      
        /** The offset is the first index of the storage that is used. */  
        private final int offset;  
      
        /** The count is the number of characters in the String. */  
        private final int count;  
      
        /** Cache the hash code for the string */  
        private int hash; // Default to 0  

      String的成员变量是private final的,即初始化之后不可改变。在这几个成员中value比较特殊,因为他是一个引用变量,而不是真正的对象。value是final修饰的,也就是说final不能再指向其他数组对象,通常我们无法访问到这个私有成员value的引用,更不能更改其数组元素。但是反射可以获取String对象中的value属性,进而改变数组结构。

    //创建字符串"Hello World", 并赋给引用s
    String s = "Hello World"; 
    
    //获取String类中的value字段
    Field valueFieldOfString = String.class.getDeclaredField("value");
    
    //改变value属性的访问权限
    valueFieldOfString.setAccessible(true);
    
    //获取s对象上的value属性的值
    char[] value = (char[]) valueFieldOfString.get(s);
    
    //改变value所引用的数组中的第5个字符: Hello_World
    value[5] = '_';

      在这个过程中,s始终引用的同一个String对象,但是再反射前后,这个String对象发生了变化。也就是说,通过反射是可以修改所谓的“不可变”对象的。但是一般我们不这么做。这个反射的实例还可以说明一个问题:如果一个对象,他组合的其他对象的状态是可以改变的,那么这个对象很可能不是不可变对象。例如一个Car对象,它组合了一个Wheel对象,虽然这个Wheel对象声明成了private final 的,但是这个Wheel对象内部的状态可以改变, 那么就不能很好的保证Car对象不可变。

    参考:

     三张图彻底了解Java中字符串的不变性

     为什么Java字符串是不可变对?

     JAVA不可变类(immutable)机制与String的不可变性

     
     

     

     

     

     

  • 相关阅读:
    【转+补充】在OpenCV for Android 2.4.5中使用SURF(nonfree module)
    Delphi StarOffice Framework Beta 1.0 发布
    Angular ngIf相关问题
    angularjs文档下载
    公众号微信支付开发
    公众号第三方平台开发 教程六 代公众号使用JS SDK说明
    公众号第三方平台开发 教程五 代公众号处理消息和事件
    公众号第三方平台开发 教程四 代公众号发起网页授权说明
    公众号第三方平台开发 教程三 微信公众号授权第三方平台
    公众号第三方平台开发 教程二 component_verify_ticket和accessToken的获取
  • 原文地址:https://www.cnblogs.com/anxiao/p/8328011.html
Copyright © 2011-2022 走看看