• 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的不可变性

     
     

     

     

     

     

  • 相关阅读:
    一个貌似比较吊的递归转换为loop--总算成功了.
    为何反转迭代顺序就不会栈溢出了?
    将树形递归转换为loop
    递归和迭代之间的转换简单例子
    非线性递归函数转化为迭代函数举例
    将尾递归函数转换为迭代函数的利器
    转:LINUX/UNIX下的回车换行与WINDOWS下的区别
    property干嘛的
    eval和列表解析的一处陷阱
    剑指offer——16二进制中1的个数
  • 原文地址:https://www.cnblogs.com/anxiao/p/8328011.html
Copyright © 2020-2023  润新知