• 深刻理解Java中final的作用(一):从final的作用剖析String被设计成不可变类的深层原因


      声明:本博客为原创博客,未经同意,不得转载!小伙伴们假设是在别的地方看到的话,建议还是来csdn上看吧(原文链接为http://blog.csdn.net/bettarwang/article/details/26744661),看代码和提问、讨论都更方便。

           Java中final的作用主要表如今三方面:修饰变量、修饰方法和修饰类。以下就从这两个方面来解说final的作用。在文末从final及类的设计安全性出发,论述了Java中String为何要被设计成不可变类。

    1.final修饰变量

          final修饰变量时与C++中的const作用类似,即假设是基本类型变量,则表示其值不能改变;假设是引用类型变量,则表示其一旦初始化,就不能再指向别的对象,可是注意它指向的对象本身的值能够改变(事实上这一点也跟C++中的常指针非常像)。看以下一个样例即知。

    import java.util.*;
    
    class Apple
    {
      private float weight;
      public Apple(float weight)
      {
        setWeight(weight);
      }
      public void setWeight(float weight)
      {
        this.weight=weight;
      }
      public void print()
      {
        System.out.println("Weight is "+String.valueOf(weight));
      }
      public void printHashCode()
      {
        System.out.println("HashCode:"+String.valueOf(hashCode()));
      }
    }
    
    public class FinalSample
    {
      public static void main(String[]args)
      {
        final int a=10;
        //对于final修饰的基本类型变量,一旦初始化后就不能再被改动
        //a=20;
        final Apple apple=new Apple(300f);
        apple.print();
        apple.printHashCode();
        //对于final修饰的引用类型变量,仅仅是其指向不能变,可是其指向的对象本身能够改变。
        apple.setWeight(350f);
        apple.print();
        apple.printHashCode();
      }
    }

    输出结果例如以下图所看到的:


    显然,final修饰的基本类型变量a是不能被改变;由输出的哈希码不变可知apple始终指向同一个对象,可是它指向的这个对象的成员weight却发生了改变。

           也正是因为final对于引用类型变量“仅仅能保证指向不变,却不能保证指向的对象本身不变”,所以在构造类时要特别小心,否则非常easy被黑客利用。看以下一个样例即知。

    import java.util.*;
    
    class AppleTag
    {
      private float weight;
      private float size;
      public AppleTag(float weight,float size)
      {
        setWeight(weight);
        setSize(size);
      }
      public void setWeight(float weight)
      {
        this.weight=weight;
      }
      public float getWeight()
      {
        return weight;
       }
      public void setSize(float size)
      {
        this.size=size;
      }
      public float getSize()
      {
        return size;
      }
    }
    public class Apple
    {
      private final AppleTag appleTag;
      public Apple(AppleTag appleTag)
      {
        this.appleTag=appleTag;
      }
      public AppleTag getAppleTag()
      {
        return appleTag;
      }
      public void print()
      {
        System.out.println("Weight:"+String.valueOf(appleTag.getWeight())+
                                " Size:"+String.valueOf(appleTag.getSize()));
      }
       
      public static void main(String[]args)
      {
        AppleTag appleTag=new AppleTag(300f,10.3f);
        Apple apple=new Apple(appleTag);
        apple.print();
       
        appleTag.setWeight(13000f);
        apple.print();
       
        AppleTag tag=apple.getAppleTag();
        tag.setSize(100.3f);
        apple.print();
      }
    
    }
    
    
    输出结果例如以下图所看到的:


    类的设计者本意是想使appleTag一旦被初始化即不被改动,而且刻意不提供set函数以防止其被篡改。但实际上类的使用者却能够从两个地方改动appleTag从而达到改变apple的目的,当中一个地方是利用构造器中的实參进行改动,还有一个就是利用getAppleTag()函数的返回值对其进行改动。

          显然,本例是因为类的设计不当而释放出过大的权限,使类的使用者将apple的重量和大小改动成了极不合理的值,从而得到错误的结果。那要怎样避免这样的错误呢?

         非常easy,就是让final变量与外界充分隔离开,如本例中使类的使用者能获取对应的值,可是无法获取appleTag,代码例如以下所看到的:

    import java.util.*;
    
    class AppleTag
    {
      private float weight;
      private float size;
      public AppleTag(float weight,float size)
      {
        setWeight(weight);
        setSize(size);
      }
      public void setWeight(float weight)
      {
        this.weight=weight;
      }
      public float getWeight()
      {
        return weight;
       }
      public void setSize(float size)
      {
        this.size=size;
      }
      public float getSize()
      {
        return size;
      }
    }
    public class Apple
    {
      private final AppleTag appleTag;
      public Apple(AppleTag appleTag)
      {
        this.appleTag=new AppleTag(appleTag.getWeight(),appleTag.getSize());
      }
      public AppleTag getAppleTag()
      {
        return new AppleTag(appleTag.getWeight(),appleTag.getSize());
      }
      public void print()
      {
        System.out.println("Weight:"+String.valueOf(appleTag.getWeight())+
                                " Size:"+String.valueOf(appleTag.getSize()));
      }
       
      public static void main(String[]args)
      {
        AppleTag appleTag=new AppleTag(300f,10.3f);
        Apple apple=new Apple(appleTag);
        apple.print();
       
        appleTag.setWeight(13000f);
        apple.print();
       
        AppleTag tag=apple.getAppleTag();
        tag.setSize(100.3f);
        apple.print();
      }
    
    }<span style="color:#ff0000;">
    
    </span>
    程序输出结果例如以下:


    显然,虽然此处类的使用者仍然尝试越权去改动appleTag,却没有获得成功,原因就在于:在接收时,在类的内部新建了一个对象并让appleTag指向它;在输出时,使用者获取的是新建的对象,保证了使用者即能够获得对应的值,又无法利用获取的对象来改动appleTag。所以apple始终未被改变,三次输出结果同样。

           在我的博客《深刻理解Java中的String、StringBuffer和StringBuilder的差别》一文中已经说过String的对象是不可变的,因而在使用String时即使是直接返回引用也不用操心使用者篡改对象的问题,例如以下例:

    import java.util.*;
    class Apple
    {
      final String type;
      public Apple(String type)
      {
        this.type=type;
      }
      public String getType()
      {
        return type;
       }
      public void print()
      {
        System.out.println("Type is "+type);
       }
    }
    
    public class AppleTest
    {
      public static void main(String[]args)
      {
        String appleType=new String("Red Apple");
        Apple apple=new Apple(appleType);
        apple.print();
    
        appleType="Green Apple";
        apple.print();
    
        String type=apple.getType();
        type="Yellow Apple";
        apple.print();
      }
    }
        
    输出结果例如以下所看到的:


    显然,与前两个样例类似,类的使用者也尝试用相似的方法去改动type从而达到改动apple的目的,可是输出结果表明这样的尝试没有成功。原因就在于String的对象不可变,在外面的改动实际上是让appleType及type分别指向了不同的对象,而用于初始化的String对象始终没有改变,当然apple中的type也就不改变了。

          再回过头来,我们发现以前让我们非常不爽的String(在深刻理解Java中的String、StringBuffer和StringBuilder的差别一文中讲到过,因为String对象不可变从而导致即使传引用也无法使对象的值改变),事实上是经过Java设计者精心设计的,试想一下,假设String对象可变的话,在我们寻常的程序编写中将会带来极大的安全隐患,而假设想杜绝这样的隐患,则每次在使用String时都要经过精密考虑,程序也会变得更复杂,而偏偏String是被大量使用的(实际上无论哪种编程语言,字符串及其处理都占有相当大的比重),显然会给我们日常的程序编写带来极大的不便。

         另外一个值得注意的地方就是数组变量名事实上也是一个引用,所以final修饰数组时,数组成员依旧能够被改变。


  • 相关阅读:
    问与答练习20210802
    jmeter向kafka中写入数据 在路上
    jmeter插件地址 在路上
    wav2vec遇到的坑:AttributeError: 'Namespace' object has no attribute 'activation'
    oracle11g+arcgis10.2.2新产品部署注意步骤
    Django OssMediaStorage 手动上传图片文件到阿里云 oss
    Django MySQL中存储表情字符
    Ubuntu conda: command not found
    Python 二进制图片数据, 转换成图片到本地
    k8s集群配置搭建skywalking
  • 原文地址:https://www.cnblogs.com/zfyouxi/p/3804654.html
Copyright © 2020-2023  润新知