java基础加强一(值传递和引用传递、String类分析)

一、Java方法的值传递和引用传递

值传递:调用方法时传的参数类型时基本数据类型(包括封装类),则不改变原本的值。

引用传递:调用方法时传递的参数类型时引用数据类型,则传递的是引用地址,原本的值改变。

原因:基本数据类型创建时在栈中,传递时复制了一个新的,同样在栈中,所以原本的值是不变的。引用数据类型创建时在栈中生成一个地址(引用),在堆中创建一个对象(空间),并把地址指向该对象,传递该方法时把地址在栈中复制,但是指向的时候还是原来的对象(空间),通过该地址修改的对象就是原来的对象,所以值会变。

误区:有人认为引用数据类型一定会改变原本的值,注意前边说的原因,其本质是复制的地址指向了同一个对象,也就是说java从来没有传递过对象,或者说java只传递值(值传递),就是栈中的值,只不过引用数据类型的值是对象地址,用来改对象的值用的。这是这个问题的本质。

特列:String类型,String类型被java设计成不可变类型,创建之后就不可变,所以原本的值不变。

示例:值传递

public static  void passValue(int i,int j){
        i++;
        j--;
    }

    public static  void testFanction(){
        int i =1;
        int j =2;
        passValue(i,j);
        System.err.println(i+"和"+j);
    }

    public static void main(String[] args) {
        testFanction();
    }

结果:1和2

分析:我们可以看到passValue接受两个参数并且进行了运算,testFanction中的i和j传给passValue后j经过运算应该变成2和1,但是打印结果还是1和2,说明passValue没有改变原本的i和j。我们看一下内存中的情况。

示例:引用传递

 public static class DemoObject{
        private  int i;
        private int j;
        public void change(){
            this.i++;
            this.j--;
        }
        public void setI(int i) {
            this.i = i;
        }
        public void setJ(int j) {
            this.j = j;
        }
        public int getI() {
            return i;
        }
        public int getJ() {
            return j;
        }
    }

    public static  void passObject(DemoObject d){
        d.change();
    }

    public static  void testFanction(){
        DemoObject d =new DemoObject();
        d.setI(1);
        d.setJ(2);
        passObject(d);
        System.out.println(d.getI()+"和"+d.getJ());

    }

    public static void main(String[] args) {
        testFanction();
    }

结果:2和1

分析:DemoObject是一个对象,testFaction在new时为它创建了一个堆空间,同时在栈中创建了引用地址,传递给passobject方法时把地址传过去就可以了,passobject方法改调用change方法改变了堆中的 值,也就是说改变了原本的值。

           

示例:String传递

public static  void passString(String s){
        s="abcde";
    }

    public static  void testFanction(){
       String s1="12345";
       passString(s1);
       System.out.println(s1);

    }

    public static void main(String[] args) {
        testFanction();
    }

结果:12345

分析:String 在java中被设计成不可变的。看源码就知道String类是通过一个char[]数组实现的,并且String类是final类型,方法也多是final类型的。sub操、concat还是replace操作都不是在原有的字符串上进行的,而是重新生成了一个新的字符串对象。也就是说进行这些操作后,最原始的字符串并没有被改变。这就是为什么使用这些方法后会有返回值,需要重新接收返回值才能实现截取。  所以在passString方法中s是一个新的对象,被赋予了新的值,以前的s1是没有被改变的。

那么问题来了,String类是每次都会创建新的对象吗?这就引出了我们的第二个知识点,String类。

二、Java的String类深入理解

  Java的String我们使用的非常多,我们知道它是一个对象数据类型,我们先来看一下API的描述:

和它的源码;

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

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;

    ........
}

我们看到,源码和API都告诉我们String类型是不可变的,它是通过一个char数组实现的,并且本身也是被final修饰,不可被继承,成员变量也是final的,也就是说String类一旦创建就不能改了,那我们截取,替换的方法是怎么实现的呢?我们看一下源码:

没错,它创建了一个新的返回给我们。

String类有11个构造方法,提供了非常多的初始化方式,但我们平时使用时最多是下面的几种方式。

  1. String s1 = new String("abc");

  2. String s2 = "def";

  3.String s3 =s1+s2;

  那么这三种方式有什么不同呢?先说结论再证明:new关键字会在堆中开辟一块空间,那么s1其实指向的是中的字符串对象。直接用“=”赋值的字符串在常量池中创建,所以s2指向的是常量池中的对象。

  “+”号是通过Stringbuder或者StringBuffer实现的拼接最后通过toString方法创建了一个新的String,这也是帮助文档的描述,所以s3指向的也是中的对象。

证明:

public static void main(String[] args) {
        String s1 =new String("abc");
        String s2 =new String("abc");
        System.out.println(s1==s2);
    }

结果:false。证明了s1和s2不是一个对象,因为new创建的在堆中,每次new都会创建一个新的,所以两次不一样。

public static void main(String[] args) {
        String s1 =new String("abc");
        String s2 ="abc";
        System.out.println(s1==s2);
    }

  结果:false。说明s1的abc和s2 的abc不是一个,那么还需要进一步证明s2是在常量池中

 public static void main(String[] args) {
        String s1 ="abc";
        String s2 ="abc";
        System.out.println(s1==s2);
    }

结果:true。因为s1创建时在常量池中,它是共享的区域,而且常量池中不能有一摸一样的两个常量存在,也就是说s1创建abc后就可以共享给别人用了,那么s2再想在创建abc字符串就会在常量池中找有没有可以共用的,找到一个一样的它就不创建了,就直接拿着之前创建的引用地址走了,所以s1和s2是同一个对象。

public static void main(String[] args) {
        String s1 ="abc";
        String s2 ="def";
        String s3="abcdef";
        String s4 =s1+s2;
        System.out.println(s3==s4);
    }

结果:false。s3是在常量池中创建,s4如过使用的也是常量中的对象就会相等,但s4是通过Stringbuffer或Stringbuilder拼接后toString生成的新对象,应该是在堆中创建,所以不等。我们可以通过反编译class文件看到其中的过程:

我们可以看到反编译的时候使用了StringBuilder,StringBuilder的toString 方法源码在右边。

说到这里,需要讨论一种特殊情况,就是字符串相加:比如 String s =“abc”+“def”;这种情况等价于String s=“abcdef”, 也就是说直接相加是在编译期就确定的了,也是在常量池中。

通过反编译我们看到,在编译成class文件时就已经确定成abcdef了。

所以说字符串因为不可变,每次都会创建新的,如果频繁使用new或者+都会造成内存的大量占用,而为了避免这种情况,jvm提供了常量池来保存字符串,可以通过共享的方式来减少创建字符串对象的开支

那么在如下的情况:

会浪费大量的内存,光new Stringbuilder 就100次。不如改成:

可以节省很多空间。所以如果不是纯字符串直接相加的话,是会使用StringBuilder的,那么涉及到长拼接我们不如直接写出最终模式,由我们控制创建的数量。

看源码可以发现,虽然StringBuilder是final的但是它继承了AbstractStringBuilder,创建字符串使用的是父类的方法,在AbstractStringBuilder中成员变量并不是final的,即StringBuilder是可变的。

我们多次提到的常量池,指的是字符串常量池,常量池分三种:字符串常量池,class常量池(静态常量池),运行时常量池(动态常量池)

JVM为了提高性能和减少内存的开销,在实例化字符串的时使用字符串常量池。创建字符串时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。由于String字符串的不可变性,常量池中一定不存在两个相同的字符串。

在JDK6及之前版本,字符串常量池是放在Perm Gen区(方法区)中的,由一个Hash表保存,叫做StringTable,它的长度是固定的1009;在JDK7版本中,字符串常量池被移到了堆中,StringTable的长度可以通过-XX:StringTableSize参数设置。所以我们不再讨论老版本的常量池。

那么什么时候放入常量池呢?答案是1.直接赋值 String s=“abc”  2.调用 构造方法new String(String original)时 3.调用String的方法intern时。这个结论是在我使用的jdk8的情况下的分析。

先来看一个经典的面试题:String s =new  String("abc") 到底创建了几个对象?

各种面试宝典的答案都是 2个 ,如果只有这一行的情况确实创建了2个,但是如果在这行前先执行了 String s1 =“abc”呢?那在执行String s =new  String("abc")时其实只创建了一个,就是堆中的String对象。

老规矩,代码证明:

左边是java代码,右边是把编译好的class文件用java自带的javap字节码查看工具查看的结果,在一定程度上反映了jvm的操作步骤,ldc是一个jvm指令,意思是将int, float或String型常量值从常量池中推送至栈顶,那么推至栈顶干什么呢,第二指令astore_1,意思是“将栈顶引用型数值存入指定本地变量”,就是将引用地址赋值给变量1,很明显这个变量就s。所以直接赋值的方式肯定是在字符串常量池中创建了对象的,不然第一条指令无法执行。

图片无法显示

可以看到new的方式,开始指令是new,即创建对象,我们知道new关键字一定会创建一个新对象,在堆中开辟空间的,所以第一个String对象就产生了,在序号4的位置又有了ldc,说明还是要去常量池中找abc的,如果没有肯定也是要创建一个对象。这样就是两个对象,一个在堆中,一个在常量池中,然后调用String的初始化,invokespecial的意思是调用超类构造方法,实例初始化方法,私有方法。

那么如果放到一起:

我们可以看到,0和2是直接赋值的指令,3、6、7是new的方式的指令,但是7的位置调用的也是#2,所以7的位置没有在常量池中创建对象,因为常量池中已经有了abc,不能创建两个完全一样的。但是new关键字还是要在堆中创建对象的,所以这情况就是创建了一个对象。你也可以把这两行对换,从指令看也是一样的,但是new先执行的,所以new先执行的话也是创建了两个。

那么为什么是new String(String s)构造方法呢?不是new String(char[] c, int start,int end);构造方法呢?因为他们的实现不一样,看源码就知道(无法显示)

可以看到第一种直接就是使用的传入的字符串的值,也就是ldc指令,需要参数先有了才能给this.value赋值,而第二种数组的方式使用的是Arrays的方式,最后也是以数组的形式存储,那么数组是在堆中,我们知道String对象是通过char数组实现,如果是字符串常量对象才会在常量池中,那么这个构造方法是没有创建常量池堆象的。这也符合String的设计,因为象subString或replace方法最后返回的新对象也是调用的这样的方法,因为如果调用new String(String o)的话要去先检查常量池中是否已经创建对象了,我们知道常字符串量池是一个StringTable,一个hash表,所以jvm为了提高性能和效率不可能在进行截取和替换这的操作还去进行这样多余的操作,如果StringTalbe很大的话,那就更完蛋了。我们也可以看javap的结果,都是数组然后new ,并没有ldc

再来说最后一个种 intern方法。这个方法就是手动入池的方法,就是如果你想把堆中的字符对象放入字符串常量池中的话就调这个方法。这里也有一个经典面试题

public static void main(String[] args) {
        String str1 = new StringBuilder("计算机").append("软件").toString();
        System.out.println(str1.intern() == str1);

        String str2 = new StringBuilder("ja").append("va").toString();
        System.out.println(str2.intern() == str2);

    }

结果:jdk1.6以前:两个false ,jdk1.7之后(我的1.8):true,false。

分析:1.6之前常量池在方法区中,调用intern方法时如果常量池不存在该字符串,虚拟机会在常量池中复制该字符串,并返回引用;如果已经存在该字符串了,则直接返回这个常量池中的这个常量对象的引用。

           1.7之后:intern方法还是会先去查询常量池中是否有已经存在,如果存在,则返回常量池中的引用,这一点与之前没有区别,区别在于,如果在常量池找不到对应的字符串,则不会再将字符串拷贝到常量池,而只是在常量池中生成一个对原字符串的引用。简单的说,就是往常量池放的东西变了:原来在常量池中找不到时,复制一个副本放到常量池,1.7后则是将在堆上的地址引用复制到常量池。

画两个图大概是这样的:

 

我们看到其实1.6之前的intern方法是要慎用的,因为可能造成常量池空间被占用过多,导致方法区内存溢出。1.7用地址代替了对象,节省了很多空间,并且将string pool放到堆中。

翻过来看这个题:

public static void main(String[] args) {
        String str1 = new StringBuilder("计算机").append("软件").toString();
        System.out.println(str1.intern() == str1);

        String str2 = new StringBuilder("ja").append("va").toString();
        System.out.println(str2.intern() == str2);

    }

首先我们知道,StringBuilder的拼接后调用toString方法生成了新的对象在堆中,但是并没有在常量池中,因为toString方法调用的构造方法是new String(char[] c,int start,int end),前面证明了这种构造器并不能创建常量池中的对象。

在1.6中“计算机软件”并没有在常量池中,str1.intern()会先检查常量池中有没有这个字符串,没有就创建新的,并返回常量池中的地址,而str1是StringBuilder的Tostring方法生成的对象的地址,所以不相同,得到false,第二个“java”是关键字,在jvm启动是就创建了,在常量池中已经存在了,那么情况就和前面一样了,intern得到了常量池中的地址,str2本身是堆中对象的地址。得到false。

在1.7中,“计算机软件”没有在常量池中,intern()会检查string pool中有没有,没有,那么会复制自己堆中的地址到常量池中,那么就是说str1和str1.nitern是一个东西,所以相同,得到了true。“java”是早就在string pool中的,所以intern直接得到了返回的string pool地址,str2是堆中地址,并不相同,得到false。

说了这么多,想必已经掌握了其中的规律。做一个小测试(1.7以后的):

 public static void main(String[] args) {
        String s1 =new String("abc")+ new String ("def");
        String s2 ="abcdef";
        System.out.println(s1==s2);
        System.out.println(s1.intern()==s2);
        System.out.println(s1.intern()==s1);

        final String s3 ="abc";
        String s4 =s3+ "def";
        System.out.println(s2==s4);
        System.out.println(s2.equals(s4));
        System.out.println(s2.hashCode()==s4.hashCode());

    }

答案:false   true    false   true  true  true。

其实做这样的题很简单,只要确定每个变量代表的是堆中的还是字符串常量池中的即可。也就是“堆常法”。

先看s1,因为使用了+ 进行了对象的相加,肯定是StringBuilder 完成的,最后创建的对象在堆中,s1代表的就是堆中的对象,再看s2,直接赋值在常量池中创建。所以s2代表常量池中的对象。

好了,这时看前三个打印,s1和s2代表不同,所以不等等,第一个false。s1.intern会检查常量池中有没有abcdef,很明显有,因为s2就代表常量池的对象,那么就返回常量池中的对象地址,所以s1.intern也代表常量池中的对象,它和s2代表相同,第二个true。那s1和s1.intern就代表不同了,第三个false。

下边的s3是个小拓展,final 关键字的作用是使变量不可变,在这里使用效果是让s3在编译期间就确定,通过反编译就可以发现 下边的s3 +“def”变成了“abc”+“def”,就成为了纯字符串相加,不会动用StringBuilder,那么s4就是“abcdef”,这样的话s4就是在字符串常量中创建,但是s2已经创建了,所以s4直接用就完了,它和s2代表相同,第四个打印true,equals方法比较的是值,不是对象,所以只要值相等就相等,打印true,String的hashcode方法依赖于equals方法,如果equals方法判定相等,那它也是相等的,打印true。思考:如果将final去掉,s2==s4是什么结果?

这里说到final,就提一嘴static,有人问我,final和static效果不一样吗?final表示不能改变,在写代码的时候写上final就是告诉别人别乱改,别改了,这就是最终。static意思是静态,被static修饰后会随着类的加载而加载,如果是方法或变量的话,就和对象无关了,只和类有关,主要是为了共享和方便调用,比如工具类里的方法大多是static的,直接类名加点调用,不需要先new对象再调用,如果static final都写上就是强调既要方便调用也不能改。

String类中涉及到了常量池,堆,栈等,这就引出了下一个知识点,jvm的内存分配。

链接:Java基础加强二

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值