Java引用类型String源码解析

目录

String解析

final的作用

String是否有长度限制

StringBuffer解析

StringBuilder解析

关键字、操作类相关


 

引用数据类型非常多大致包括:类、 接口类型、 数组类型、 枚举类型、 注解类型、 字符串型。String类型就是引用类型。

String解析

JVM运行时会分配一块空间给String,字符串的分配和其他对象分配一样,需要消耗高昂的时间和空间,JVM为了提高性能和减少内存的开销,在实例化字符串的时候进行了一些优化,使用字符串常量池,创建字符串常量时,JVM先检查字符串常量池中有没有,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用,如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。String类是由final关键字修饰的,字符串具有不可变性,常量池中不会存在两个相同的字符串。

      public class App {
          public static void main(String[] args) {
              String a = "111";
              a = "222";
              System.out.println(a);
          }
      }

引用类型声明的变量是指该变量在内存中实际存储的是一个引用地址,实体在堆中。所以上面String a = “111”,表达的是变量a里保存了“111”这个对象的引用地址,变量是可以变的,不能变的是“111”。a="222",先去JVM常量池中查找,如果常量池中存在,就直接把对象的引用地址赋给a,如果不存在就重新创建一个对象,然后把对象的引用地址赋给a。

final的作用

当用final修饰一个类时,表明这个类不能被继承。final修饰的类中的成员变量可以根据需要设为final(类中所有成员方法都会被隐式地指定为final方法)。final修饰的方法表示此方法已经是“最后的、最终的”含义,即此方法不能被重写,但可以重载。重写的前提是子类可以从父类中继承此方法,如果父类中final修饰方法同时访问控制权限为private,会导致子类中不能直接继承到此方法,此时可以在子类中定义相同的方法名和参数(类的private方法会隐式地被指定为final方法)。当final修饰一个基本数据类型时,表示该基本数据类型的值一旦在初始化后便不能发生变化。如果final修饰一个引用类型时,则在对其初始化之后便不能再让其指向其他对象了,但该引用所指向的对象的内容是可以发生变化的。另外final修饰一个成员变量必须要初始化。有两种初始化方式(在声明的时候给其赋值,在其类的所有构造方法中为其赋值)

    public class FinalDemo {
        private final String name;//1

        public FinalDemo(String name) {//2
            this.name = name;//3
        }

        public FinalDemo() {//4
        }
    }

1处不会通过编译,要先给name初始化值才可以通过编译。

181340de35f4409bb3a0b186bf98ed2c.png

 

String几个常用方法源码

       public String concat(String str) {
              int otherLen = str.length();
              if (otherLen == 0) {
                  //啥都没有,就直接把当前字符串给你
                  return this;
              }
              int len = value.length;
              char buf[] = Arrays.copyOf(value, len + otherLen);
              str.getChars(buf, len);
              //看到了吗?返回的居然是新的String对象
              return new String(buf, true);
          }
          void getChars(char dst[], int dstBegin) {
              System.arraycopy(value, 0, dst, dstBegin, value.length);
          }    
          public String replace(char oldChar, char newChar) {
              //如果两个是一样的,那就必要替换了,所以返回this
              if (oldChar != newChar) {
                  int len = value.length;
                  int i = -1;
                  //把当前的char数组复制给val,然后下面基于val来操作
                  char[] val = value; 
                  while (++i < len) {
                      if (val[i] == oldChar) {
                          break;
                      }
                  }
                  if (i < len) {
                      //创建一个新的char数组
                      char buf[] = new char[len];
                      for (int j = 0; j < i; j++) {
                          buf[j] = val[j];
                      }
                      while (i < len) {
                          char c = val[i];
                          buf[i] = (c == oldChar) ? newChar : c;
                          i++;
                      }
                      //创建一个新的String对象
                      return new String(buf, true);
                  }
              }
              return this;
          }
          public String substring(int beginIndex, int endIndex) {
              if (beginIndex < 0) {
                  throw new StringIndexOutOfBoundsException(beginIndex);
              }
              if (endIndex > value.length) {
                  throw new StringIndexOutOfBoundsException(endIndex);
              }
              int subLen = endIndex - beginIndex;
              if (subLen < 0) {
                  throw new StringIndexOutOfBoundsException(subLen);
              }
              //正常返回的都是新new出来的String对象
              return ((beginIndex == 0) && (endIndex == value.length)) ? this
                      : new String(value, beginIndex, subLen);
          }
          public String trim() {
              int len = value.length;
              int st = 0;
              char[] val = value;    /* avoid getfield opcode */
              while ((st < len) && (val[st] <= ' ')) {
                  st++;
              }
              while ((st < len) && (val[len - 1] <= ' ')) {
                  len--;
              }
              //如果是该字符串中包含了空格,调用substring方法,否则就是啥都没干原本返回
              //就是如果字符串里有空格,那么还是新生一个String对象返回
              return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
          }

无论是concat、replace、substring还是trim方法的操作都不是在原有的字符串上进行的而是重新生成了一个新的字符串对象。也就是说进行这些操作后,最原始的字符串并没有被改变。String对象一旦被创建就是固定不变的了,对String对象的任何改变都不影响到原对象,任何变化性的操作都会生成新的对象。

    public class App {
        public static void main(String[] args) {
            String a = "111";
            String a1 = "111";

            String b = new String("111");

            //对象地址是同一个 
            //==比的是变量的值(值:指向内容的引用地址),equals比的是变量的内容
            System.out.println(a==a1);
            //对象内容是一样的
            System.out.println(a.equals(a1));
            //对象地址不一样
            System.out.println(a==b);
            //对象内容是一样的
            System.out.println(a.equals(b));
        }
    }

结果解析 

输出结果:true true false true 
第一个输出true,a和a1两个变量保存的引用地址是同一个。
第二个输出true,a和a1引用地址中内容是一样的。
String a = "111"在JVM申请内存存放"111"对应的对象,当String a1="111"的时候,先去JVM里寻找是否存在"111",如果存在直接把对象的引用地址给a1。此时的a和a1都保存着同一个引用地址。String b = new String("111")创建一个对象然后把对象引用地址赋给变量b,先去JVM里找 "111",找到了直接存放引用地址。找不到创建一个对象然后把引用地址给String的有参构造方法里。所以第三个中输出false,因为a和b所保存的对象引用是不一样的。
最后一个输出true。那是因为两个变量所保存的引用地址中的内容都是“111”。

String是否有长度限制

在Java中String是有长度限制的,在JVM编译中有规范。String长度限制的场景:将某固定文件转码成Base64的形式用字符串存储,运行时需要的时候在转回来,文件比较大。String a = "ssssssss..."构造的10万个字符的字符串,编译之后虚拟机提示报错,提示字符串长度过长。字符串的内容是由一个字符数组 char[] 来存储的,由于数组的长度及索引是整数且String类中返回字符串长度的方法length()返回值也是int ,通过int类型对应的包装类Integer源码中可以看到其长度最大限制为2^31 -1,说明数组的长度是0~2^31-1,那么大小就是(2^31-1 = 2147483647 = 2GB)。 但是通过翻阅java虚拟机手册对class文件格式的定义以及常量池中对String类型的结构体定义,对于索引定义了u2,就是无符号占2个字节,2个字节可以表示的最大范围是2^16 -1 = 65535, 但是JVM需要1个字节表示结束指令,所以这个范围就为65534了。超出这个范围在编译时期是会报错。

StringBuffer解析

StringBuffer是可变的字符序列,当一个StringBuffer被创建以后,通过StringBuffer提供append()、insert()、reverse()、setCharAt()、setLength()等方法可以改变这个字符串对象的字符序列。一旦通过StringBuffer生成了最终想要的字符串,就可以调用它的toString()方法将其转换为一个String对象。StringBuffer的直接父类是AbstractStringBuilder,实现了Serializable即StringBuffer的对象可以串行化,在父类中AbstractStringBuilder有属性char[] value,不是final,该value数组存放字符串内容,存放在堆中,StringBuffer是一个final 类,不能被继承,因为StringBuffer字符内容是存在char[] value所以在变化(增加/删除) 时,不用每次都更换地址(不用创建新对象)效率高于String。

/**
 * @Author 
 * StringBuffer
 **/
public class StringBuffer01 {
    public static void main(String[] args) {
        //创建一个大小为16的char[],用于存放字符内容
        StringBuffer stringBuffer01 = new StringBuffer();
        
        //2.通过构造器指定char[]的大小
        StringBuffer stringBuffer02 = new StringBuffer(100);

        //通过给一个String 创建 StringBuffer,char[] 大小就是str.length + 16
        StringBuffer stringBuffer03 = new StringBuffer("hello");

        String str = null;
        StringBuffer sb = new StringBuffer();
        sb.append(str);
        System.out.println(sb.length());//4
    }
}

String str = null是成立的,但是在StringBuffer的源码中append()方法:

@Override
    public synchronized StringBuffer append(Object obj) {
        toStringCache = null;
        super.append(String.valueOf(obj));
        return this;
    }

从源码中看出StringBuffer中的append方法调用了父类的append方法,进父类AbstractStringBuilder查看父类的append方法,源码如下:

    public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        ensureCapacityInternal(count + len);
        str.getChars(0, len, value, count);
        count += len;
        return this;
    }

str为空时,调用appendNull()方法,追进appendNull()方法

  private AbstractStringBuilder appendNull() {
        int c = count;
        ensureCapacityInternal(c + 4);
        final char[] value = this.value;
        value[c++] = 'n';
        value[c++] = 'u';
        value[c++] = 'l';
        value[c++] = 'l';
        count = c;
        return this;
    }

把空对象转化为字符数组’null’,故最后输出的结果应该为4。

StringBuilder解析

StringBuilder和StringBuffer相似,两个类的构造器和方法也基本相同。不同的是StringBuffer是线程安全的,StringBuilder没有实现线程安全功能,所以性能略高。StringBuffer类中的方法都添加了synchronized关键字,给方法添加了一个锁,用来保证线程安全。Java9改进了字符串(包括String、StringBuffer、StringBuilder)的实现。在Java9以前字符串采用char[]数组来保存字符,字符串的每个字符占2字节,Java9的字符串采用byte[]数组再加一个encoding-flag字段来保存字符,字符串的每个字符只占1字节,所以Java9的字符串更加节省空间。

StringBuilder源码

    @Override
    @HotSpotIntrinsicCandidate
    public StringBuilder append(String str) {
        super.append(str);
        return this;
    }

是什么导致了StringBuilder的线程不安全,进入父类AbstractStringBuilder

    public AbstractStringBuilder append(String str) {
        if (str == null) {
            return appendNull();
        }
        int len = str.length();
        ensureCapacityInternal(count + len);
        putStringAt(count, str);
        count += len;
        return this;
    }

问题在这两行

ensureCapacityInternal(count + len);
putStringAt(count, str); 

ensureCapacityInternal() 是一个扩容方法

    private void ensureCapacityInternal(int minimumCapacity) {
        // overflow-conscious code
        int oldCapacity = value.length >> coder;
        if (minimumCapacity - oldCapacity > 0) {
            value = Arrays.copyOf(value,
                    newCapacity(minimumCapacity) << coder);
        }
    }

coder是字符的编码格式,编码默认是Latin1,对应的coder是0。还有一种编码UTF16,对应的coder为1。用count(已经使用的长度)+len(要拼接的长度)得到需要的最小长度minimumCapacity。如果这个长度比原来的容量大,则触发扩容,把原数组复制到一个容量为(minimumCapacity*2+2)的新数组。在并发情况下,可能有多个线程拿到相同count,导致扩容不充分引起数组下标越界异常。一般要三个以上线程同时拿到count且必须是在程序开始时,数组不大的时候才可能出现这个异常。再来看putStringAt():

    private final void putStringAt(int index, String str) {
        if (getCoder() != str.coder()) {
            inflate();
        }
        str.getBytes(value, index, coder);
    }

在多线程下,可能有多个线程拿到相同count,在执行getBytes时,这几个线程添加的位置是相同的,可能会发生数据覆盖的情况。StringBuilder线程安全测试示例:

    @Test
    public void stringDemo02() throws InterruptedException {
        CountDownLatch count=new CountDownLatch(10000);
        StringBuilder stringBuilder=new StringBuilder();
        for (int i = 0; i <100 ; i++) {
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j <100 ; j++) {
                        stringBuilder.append("q");
                        count.countDown();
                    }
                }
            });
            t.start();
        }
        count.await();
        System.out.println(stringBuilder.length());
    }
9912

关键字、操作类相关

final关键字:final修饰的类叫最终类,该类不能被继承。final 修饰的方法不能被重写。final 修饰的变量叫常量,常量必须初始化,初始化之后值就不能被修改。

操作字符串的类有:String、StringBuffer、StringBuilder。String和StringBuffer、StringBuilder的区别在于String声明的是不可变的对象,每次操作都会生成新的String对象,然后将指针指向新的 String对象,而StringBuffer、StringBuilder可以在原有对象的基础上进行操作,所以在经常改变字符串内容的情况下最好不要使用String。 StringBuffer和StringBuilder最大的区别在于StringBuffer 是线程安全的而StringBuilder是非线程安全的,StringBuilder的性能高于StringBuffer,在单线程环境下推荐使用StringBuilder,多线程环境下推荐使用StringBuffer。String str="i"与String str=new String("i")区别,String str="i"的方式,Java 虚拟机会将其分配到常量池中,String str=new String("i") 则会被分到堆内存中。

字符串反转

              使用 StringBuilder 或者 stringBuffer 的 reverse() 方法。
              // StringBuffer reverse
              StringBuffer stringBuffer = new StringBuffer();
              stringBuffer. append("abcdefg");
              System. out. println(stringBuffer. reverse()); // gfedcba
              // StringBuilder reverse
              StringBuilder stringBuilder = new StringBuilder();
              stringBuilder. append("abcdefg");
              System. out. println(stringBuilder. reverse()); // gfedcba

 String类的常用方法:

              indexOf():返回指定字符的索引。
              charAt():返回指定索引处的字符。
              replace():字符串替换。
              trim():去除字符串两端空白。
              split():分割字符串,返回一个分割后的字符串数组。
              getBytes():返回字符串的 byte 类型数组。
              length():返回字符串长度。
              toLowerCase():将字符串转成小写字母。
              toUpperCase():将字符串转成大写字符。
              substring():截取字符串。
              equals():字符串比较。

普通类和抽象类区别

普通类不能包含抽象方法,抽象类可以包含抽象方法,抽象类不能直接实例化,普通类可以直接实例化。抽象类不能使用final修饰,定义抽象类就是让其他类继承的,如果定义为final该类就不能被继承,这样彼此就会产生矛盾,所以final不能修饰抽象类。

接口和抽象类区别

抽象类的子类使用extends来继承,接口必须使用implements来实现接口。抽象类可以有构造函数,接口不能有。类可以实现很多个接口,但是只能继承一个抽象类。接口中的方法默认使用 public修饰,抽象类中的方法可以是任意访问修饰符。

泛型

泛型就是可以适应不同的类型,这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。保证了类型的安全性:泛型约束了变量的类型,保证了类型的安全性。例如List和ArrayList。List集合只能加入int类型的变量,ArrayList可以Add任何常用类型,编译的时候不会提示错误。泛型能够省去类型强制转换。提高方法、算法的重用性。

            //泛型类
            public class GenericClass<T> {
              private T value;
              public GenericClass(T value) {
                  this.value = value;
              }
              public T getValue() {
                  return value;
              }
              public void setValue(T value) {
                  this.value = value;
              }
             }
             
            //泛型接口
            public interface GenericInterface<T> {
             void show(T value);
           }
           
           //泛型方法
          public class GenericFun {
              public void show(String value) { }
              public void show(Integer value) { }
          }

 

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/171803.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

SpringCloud原理-OpenFeign篇(一、Hello OpenFeign项目示例)

文章目录 前言正文一、项目结构二、服务调用链路说明三、Rpc调用链路说明四、项目代码4.1 client 模块中的feign接口4.2 client 中的rest接口4.3 client 中的启动类4.4 server中的rest接口4.5 server中的配置文件 五、调试 前言 本篇是SpringCloud原理系列的 OpenFeign 模块的…

SpringBoot:ch02 配置文件(日志)

前言 简单介绍 Spring Boot 中常见的配置文件类型&#xff0c;如 application.properties 和 application.yml 等&#xff0c;并说明它们各自的特点和用途。 一、前期准备 1、新建项目&#xff0c;结构如下 2、添加依赖 <?xml version"1.0" encoding"UTF…

如今 Android 开发都要转去做鸿蒙开发了吗?

近期&#xff0c;华为的鸿蒙&#xff08;Harmony OS&#xff09;操作系统引起了广泛的关注&#xff0c;一是被编写进了许多大学课程&#xff1b;二是不少互联网大厂在为布局鸿蒙系统而“招兵买马”。像美团、京东、网易、今日头条……等知名的互联网大厂&#xff0c;都已经发布…

Linux下Centos7 gcc/g++、动态库/静态库(动态/静态链接)

1.gcc/g gcc是对c语言代码进行编译链接&#xff0c;而g是对c代码进行编译链接&#xff0c;接下来我们只对gcc进行讲解&#xff0c;g的使用方法跟gcc是一样的。 编译链接的四个步骤: 1:预处理 2:编译 3:汇编 4:链接 注&#xff1a;这些在后面都会着重讲解 1.1gcc -o 我们先在D…

电机应用开发-编码器的使用

编码器 增量式编码器倍频技术 增量式编码器输出的常见脉冲波形信号形式&#xff1a; 占空比为50%的方波&#xff0c;通道A和通道B相位差为90。 正弦波的模拟信号&#xff0c;通道A和通道B相位差为90。 对于占空比为50%的方波&#xff0c;通道A和通道B相位差为90。先以下图为例…

Python实现WOA智能鲸鱼优化算法优化随机森林回归模型(RandomForestRegressor算法)项目实战

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档视频讲解&#xff09;&#xff0c;如需数据代码文档视频讲解可以直接到文章最后获取。 1.项目背景 鲸鱼优化算法 (whale optimization algorithm,WOA)是 2016 年由澳大利亚格里菲斯大学的Mirjalili 等提…

基于单片机设计的气压与海拔高度检测计(采用MPL3115A2芯片实现)

一、前言 随着科技的不断发展&#xff0c;在许多领域中&#xff0c;对气压与海拔高度的测量变得越来越重要。例如&#xff0c;对于航空和航天工业、气象预报、气候研究等领域&#xff0c;都需要高精度、可靠的气压与海拔高度检测装置。针对这一需求&#xff0c;基于单片机设计…

基于SSM的学院网站设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;Vue 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#xff1a;是 目录…

2024年测试工程师必看比列之Unittest单元测试框架-知识点总结

unittest单元测试框架 1.导入unittest包 2.创建类的时候要继承与unittest.TestCase类 2.1&#xff0c;setUp方法是在类中测试执行前的初始化工作 2.2&#xff0c;tearDown方法是在类中测试执行后的清除工作 2.3&#xff0c;测试用例函数以test开头的方法是普通的测试用例方法&…

基于单片机的公共场所马桶设计(论文+源码)

1.系统设计 本课题为公共场所的马桶设计&#xff0c;其整个系统架构如图2.1所示&#xff0c;其采用STC89C52单片机为核心控制器&#xff0c;结合HC-SR04人体检测模块&#xff0c;压力传感器&#xff0c;LCD1602液晶&#xff0c;蜂鸣器&#xff0c;L298驱动电路等构成整个系统&…

通达信吊灯止损指标公式,根据波动幅度自动调整止盈止损

吊灯止损指标是由查克勒博(Chuck LeBeau)发明的&#xff0c;亚历山大埃尔德(Alexander Elder)在其著作《走进我的交易室》中介绍了这种止盈止损方法&#xff08;中文版翻译为倒挂式离场法则&#xff09;&#xff0c;它是根据平均真实波幅ATR设置跟踪止损。吊灯止损指标的目的是…

使用 LCM LoRA 4 步完成 SDXL 推理

LCM 模型 通过将原始模型蒸馏为另一个需要更少步数 (4 到 8 步&#xff0c;而不是原来的 25 到 50 步) 的版本以减少用 Stable Diffusion (或 SDXL) 生成图像所需的步数。蒸馏是一种训练过程&#xff0c;其主要思想是尝试用一个新模型来复制源模型的输出。蒸馏后的模型要么尺寸…

论文阅读:“基于特征检测与深度特征描述的点云粗对齐算法”

文章目录 摘要简介相关工作粗对齐传统的粗对齐算法基于深度学习的粗对齐算法 特征检测及描述符构建 本文算法ISS 特征检测RANSAC 算法3DMatch 算法 实验结果参考文献 摘要 点云对齐是点云数据处理的重要步骤之一&#xff0c;粗对齐则是其中的难点。近年来&#xff0c;基于深度…

VM——绘制亮度均匀性曲线

1、需求:检测汽车内饰氛围灯的亮度均匀性,并绘制均匀性曲线 2、结果: 3、方法: 主要分为3步 (1)提取氛围灯ROI,忽略背景 (2)对提取到的ROI图进行切片处理,计算出每个切片的亮度均值 (3)绘制均匀性曲线 3.1 提取氛围灯ROI step1: 转成黑白图 step2:通过blob和…

【图像分类】【深度学习】【Pytorch版本】Inception-ResNet模型算法详解

【图像分类】【深度学习】【Pytorch版本】Inception-ResNet模型算法详解 文章目录 【图像分类】【深度学习】【Pytorch版本】Inception-ResNet模型算法详解前言Inception-ResNet讲解Inception-ResNet-V1Inception-ResNet-V2残差模块的缩放(Scaling of the Residuals)Inception-…

第1关:图的邻接矩阵存储及求邻接点操作

任务要求参考答案评论2 任务描述相关知识 顶点集合边集合编程要求测试说明 任务描述 本关任务&#xff1a;要求从文件输入顶点和边数据&#xff0c;包括顶点信息、边、权值等&#xff0c;编写程序实现以下功能。 1&#xff09;构造无向网G的邻接矩阵和顶点集&#xff0c;即图…

配置文件自动提示

1、引入依赖<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId> </dependency> 2、修改IDEA配置

mysql底层是如何存放数据的

总览 首先总的来说&#xff0c;分为四个层级&#xff0c;行页区段。行就是数据库里的一行数据。 但一次从磁盘读进内存的数据量是一页&#xff08;页是读写的单位&#xff0c;默认16KB一页&#xff09;&#xff0c;页分很多种类&#xff0c;例如数据页、溢出页、undo日志页。 …

OpenAI宫斗,尘埃落定,微软成最大赢家

周末被OpenAI董事会闹剧刷屏,ChatGPT之父Sam Altman前一天被踢出董事会,免职CEO,后一天重返OpenAI,目前结局未知。 很多同学想要围观,缺少背景知识,这里老章为大家简单介绍前因后果及涉及的人物,时间线,让大家轻松围观。 备好瓜子,开始。 1、主角 先看一张图,看一…