Java基础面试总结(一)

  • JDK 8 新特性
    • lambda表达式
      • lambda表达式允许你通过表达式来代替函数式接口,lambda表达式就和方法一样,它提供了一个正常的参数列表和一个方法体(body,可以是一个表达式或一个代码块)。Lamda表达式是由函数式接口所支持的,函数式接口是只有一个抽象方法的接口,是Lamda表达式的类型。一个lambda包括三部分:
      • 一个括号内用逗号分隔的形式参数,参数是函数式接口里面方法的参数;
      • 一个箭头符号:->
      • 方法体,可以是表达式和代码块,方法体是函数式接口里面方法的实现:如果是代码块,则必须用{}来包裹起来,且需要一个return返回值,但有个例外,若函数式接口里面方法返回值是void,则无需{}。
  • Java四个基本特性
    • 重写(override)又名覆盖:
      • 1.不能存在同一个类中,在继承或实现关系的类中;
      • 2. 名相同,参数列表相同,方法返回值相同,
      • 3.子类方法的访问修饰符要大于父类的。
      • 4.子类的检查异常类型要小于父类的检查异常。
    • 重载(overload)
      • 1.可以在一个类中也可以在继承关系的类中;
      • 2.名相同;
      • 3.参数列表不同(个数,顺序,类型) 和方法的返回值类型无关。 
    • 接口和抽象类的区别:
      • 相同点:
        • 都位于继承的顶端,用于被其他类实现或继承;
        • 都不能直接实例化对象;
        • 都包含抽象方法,其子类都必须覆写这些抽象方法;
      • 区别:
        • 抽象类为部分方法提供实现,避免子类重复实现这些方法,提高代码重用性;接口只能包含抽象方法;
        • 一个类只能继承一个直接父类(可能是抽象类),却可以实现多个接口;(接口弥补了Java的单继承)
        •  抽象类是这个事物中应该具备的你内容, 继承体系是一种 is..a关系(extends)
        •  接口是这个事物中的额外内容,继承体系是一种 has..a关系(implements)
        • 接口中成员变量默认为public static final 且必须赋值。所有方法必须为public abstract。抽象类可可以有成员变量(default,默认·本包可见),也可以非抽象的成员方法(必须有abstract修饰,分毫结尾,不能带{})
      • 当子类构造函数需要调用父类构造函数时,super()必须为构造函数中第一条语句;
  • 多态的底层实现
    • JVM 的方法调用指令有五个,分别是:
      • invokestatic:调用静态方法;
      • invokespecial:调用实例构造器<init>方法、私有方法和父类方法;
      • invokevirtual:调用虚方法;
      • invokeinterface:调用接口方法,运行时确定具体实现;
      • invokedynamic:运行时动态解析所引用的方法,然后再执行,用于支持动态类型语言。
      • 其中,invokestatic 和 invokespecial 用于静态绑定,invokevirtual 和 invokeinterface 用于动态绑定。可以看出,动态绑定主要应用于虚方法和接口方法。
      • 静态绑定在编译期就已经确定,这是因为静态方法、构造器方法、私有方法和父类方法可以唯一确定。这些方法的符号引用在类加载的解析阶段就会解析成直接引用。因此这些方法也被称为非虚方法,与之相对的便是虚方法。
      • 虚方法的方法调用与方法实现的关联(也就是分派)有两种,
        • 一种是在编译期确定,被称为静态分派,比如方法的重载;
        • 一种是在运行时确定,被称为动态分派,比如方法的覆盖。对象方法基本上都是虚方法。
      • 这里需要特别说明的是,final 方法由于不能被覆盖,可以唯一确定,因此 Java 语言规范规定 final 方法属于非虚方法,但仍然使用 invokevirtual 指令调用。静态绑定、动态绑定的概念和虚方法、非虚方法的概念是两个不同的概念。
    • 多态(重载)的实现:
      • (1)虚拟机在重载时时通过参数的静态类型而不是实际类型来决定使用哪一个重载版本。
      • (2)所有依赖静态类型来定位方法来执行版本的分派动作称为静态分派,静态分派的典型应用是方法重载。静态分派发生在编译阶段。
    • 多态的(重写)实现:
      • 虚拟机栈中会存放当前方法调用的栈帧,在栈帧中,存储着局部变量表、操作栈、动态连接 、返回地址和其他附加信息。多态的实现过程,就是方法调用动态分派的过程,通过栈帧的信息去找到被调用方法的具体实现,然后使用这个具体实现的直接引用完成方法调用。
      • 以 invokevirtual 指令为例,在执行时,大致可以分为以下几步:
        • 先从操作栈中找到对象的实际类型 class;
        • 找到 class 中与被调用方法签名相同的方法,如果有访问权限就返回这个方法的直接引用,如果没有访问权限就报错 java.lang.IllegalAccessError ;
        • 如果第 2 步找不到相符的方法,就去搜索 class 的父类,按照继承关系自下而上依次执行第 2 步的操作;
        • 如果第 3 步找不到相符的方法,就报错 java.lang.AbstractMethodError ;
        • 可以看到,如果子类覆盖了父类的方法,则在多态调用中,动态绑定过程会首先确定实际类型是子类,从而先搜索到子类中的方法。这个过程便是方法覆盖的本质。
        • (1)在常量池中找到方法调用的符号引用
        • (2)查看父类的方法表,得到方法在该方法表的偏移量(假设为15),这样就得到该方法的直接引用。
        • (3)根据this指针确定方法接收者(girl)的实际类型
        • (4)根据对象的实际类型得到该实际类型对应的方法表,根据偏移量15查看有无重写(override)该方法,如果重写,则可以直接调用;如果没有重写,则需要拿到按照继承关系从下往上的基类(这里是父类类)的方法表,同样按照这个偏移量15查看有无该方法。
        • 实际上,商用虚拟机为了保证性能,通常会使用虚方法表和接口方法表,而不是每次都执行一遍上面的步骤。以虚方法表为例,虚方法表在类加载的解析阶段填充完成,其中存储了所有方法的直接引用。也就是说,动态分派在填充虚方法表的时候就已经完成了。
        • 在子类的虚方法表中,如果子类覆盖了父类的某个方法,则这个方法的直接引用指向子类的实现;而子类没有覆盖的那些方法,比如 Object 的方法,直接引用指向父类或 Object 的实现。
  • Java变量作用域
    • 1)类的成员变量当类被实例化后,成员变量会在内存中分配空间并初始化,当类实例化对象生命周期结束,变量结束;
    • 2)被static修饰的变量成为静态变量或全局变量,静态变量不依附与实例,而被所有实例共享。
    • 3)public作用于当前类,同包,子类,其他包;
    • 4)private作用于当前类;
    • 5)protected作用于当前类,同包,子类;
    • 6)default作用于当前类和同包;
    • 7)private和protected不能用于修事类,只有public、abstract、final可用于修事修。
  • 构造函数:用于对象实例化时初始化对象的成员变量。
    • 1)构造函数名字必须与类名相同,也不能有返回值;
    • 2)构造函数可以被重载但不能被继承;
    • 3)只能通过new自动调用。
  • 基本类型和引用类型做参数传递
    • 成员变量
      • 编译看左边(父类),运行看左边(父类)。

    • 成员方法
      • 编译看左边(父类),运行看右边(子类)。
    • 静态方法
      • 编译看左边(父类),运行看左边(父类)。
      • 静态和类相关,算不上重写,所以,访问还是左边的
      • 只有非静态的成员方法,编译看左边,运行看右边
    • 方法参数传递时(基本类型+String和引用类型的区别)
      • 在使用基本类型作为方法的参数进行传递时,main进栈,然后方法进栈,main中的定义变量传入方法中,改变方法中的变量,然后方法出栈不会影响main中的变量值。

      • 在使用引用类型进行参数传递的时候,实际上会改变堆内存中对象的数值
      • 基本数据类型和引用数据类型作为参数的区别
        • 基本数据类型的变量中直接存放数据值本身,所以改的时候改的是数据值本身;
        • 但是引用类型不同的地方在于真正的数据并没有在栈区的变量中保存,而是在堆区里面保存着,所以虽然也拷贝了一份,也是副本,但是二者指向的是同一块堆区。
        • 引用数据类型就好比如说,两位同学使用的是同一份复习资料,其中一人把资料撕毁了,另一人当然也会受到影响。
        • 而基本数据类型就好比复印了一份,其中一人将自己的资料撕了,并不影响别人。
      • 总结:
        • 1).当使用基本数据类型作为方法的形参时,在方法体中对形参的修改不会影响到实参的数值
        • 2).当使用引用数据类型作为方法的形参时,若在方法体中修改形参指向的数据内容,则会
          • * 对实参变量的数值产生影响,因为形参变量和实参变量共享同一块堆区;*
        • 3).当使用引用数据类型作为方法的形参时,若在方法体中修改形参变量的指向,此时不会
          • * 对实参变量的数值产生影响,因此形参变量和实参变量分别指向不同的堆区;*
  • Java 自动装箱、拆箱机制
    • Java为每种基本数据类型(8种)都提供了对应的包装器类型。所谓自动装箱机制就是自动将基本数据类型转换为包装器类型,而自动拆箱机制就是自动将包装器类型转换为基本数据类型。在JDK中,装箱过程是通过调用包装器的valueOf方法实现的,而拆箱过程是通过调用包装器的 xxxValue方法实现的(xxx代表对应的基本数据类型)。
      • 在装箱时,valueOf方法会被自动调用:如果整型字面值在[-128,127]之间,便返回 IntegerCache.cache(在类加载时就自动创建) 中已经存在的对象的引用;否则,创建一个新的Integer对象并返回。

      • Integer、Short、Byte、Character、Long 这几个类的valueOf方法的实现是类似的,有限可列举,共享[-128,127];
      • Double、Float的valueOf方法的实现是类似的,无限不可列举,不共享;

      • Boolean的valueOf方法的实现不同于以上的整型和浮点型,只有两个值,有限可列举,共享;

      • “==”运算符:当使用“==”运算符在基本类型和其包装类对象之间比较时,涉及到自动装箱、拆箱机制,遵循如下规则:
        • 1). 只要两个操作数中有一个是基本类型或表达式(即包含算术运算符),就是比较它们的数值是否相等。
        • 2). 否则,就是判断这两个对象的内存地址是否相等,即是否是同一个对象。

        • 第一个和第二个输出结果没有什么疑问;第三个打印语句由于 a+b 包含了算术运算,因此会触发自动拆箱过程(会调用intValue方法),因此它们比较的是数值是否相等;
        • 对于c.equals(a+b)会先触发自动拆箱过程,再触发自动装箱过程,也就是说a+b,会先各自调用intValue方法,得到了加法运算后的数值之后,便调用Integer.valueOf方法,再进行equals比较;对于g==(a+b),会分别触发 Integer 和 Long 的自动拆箱过程,然后 int 自动转为 long,进行比较;对于g.equals(a+b),最终会归结于 Long对象与Integer对象的比较,由于二者不为同一类型,直接返回 false ;对于g.equals(a+h),最终会归结于 Long对象与Long对象的比较,由于 -128 <= 3 <= 127, 二者为同一对象,直接返回 true 。
      • short s1 = 1; s1 = s1 + 1; 有 什 么 错 ? short s1 = 1; s1+=1;有什么错?
        • 1) 对于 short s1=1;s1=s1+1 来说,在s1+1 运算时会自动提升表达式的类型为 int,那么将 int 赋予给 short 类型的变量 s1 会出现类型转换错误。
        • 2) 对于 short s1=1;s1+=1 来说 +=是 java 语言规定的运算符,java 编译器会对它进行特殊处理,因此可以正确编译。short s1 = (short)(s2 + (short)1);
      • int 和 Integer
        • 1、Integer是int的包装类,int则是java的一种基本数据类型
        • 2、Integer变量必须实例化后才能使用,而int变量不需要
        • 3、Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象;而int则是直接存储数据值
        • 4、Integer的默认值是null,int的默认值是0
        • 5、引申
      • boolean byte 1字节 8bit byte(-128---+127)
      • char short 2字节 16bit
      • int float 4字节 32bit
      • long double 8字节 64bit
    • 自动转型与强制转型
      • 1、 自动转型:自动转型总原则:byte,short,char(同级)-> int -> long -> float -> double (由低精度到高精度)
        • 从位数低的类型向位数高的类型转换

        • 从整型向浮点型的转换

        • 运算符对基本类型的影响
          • 1) 当使用 +、-、*、/、%、==、>、< 等 等运算符对基本类型进行运算时,遵循如下规则:两个操作数中,先考虑是否有一个是double类型的。如果有,另一个操作数和结果 将会被转换成double类型。再依次考虑float,long。除此之外,两个操作数(包括byte、short、int、char)都将会被转换成int类型。

          • 2)当使用 +=、-=、*=、/=、%= 、 i++、 ++i 运算符对基本类型进行运算时,遵循如下规则:运算符右边的数值将首先被强制转换成与运算符左边数值相同的类型,然后再执行运算,且运算结果与运算符左边数值类型相同。自增(减)运算也类似。

      • 2、 强制转型:强制转换的格式是在需要转型的数据前加上 “( )”, 然后在括号内加入需要转化的数据类型。主要发生于以下两种情形:

        • 由高精度向低精度转换
        • 一种类型到另一种类型转换,则必须使用强制类型转化(同级之间:byte,short,char)
  • Object的方法如下:
    • 1.hashCode和equals函数用来判断对象是否相同 
      • 1.1     ==
        • 基本数据类型:(==),比较的是他们的值。
        • 引用类型(类、接口、数组)   :用(==)进行比较的时候,比较的是他们在内存中的存放地址,所以,除非是同一个new出来的对象,他们的比较后的结果为true,否则比较后结果为false。对象是放堆中的栈中存放的是对象的引用(地址)。由此可见'=='是对栈中的值进行比较的。如果要比较堆中对象的内容是否相同,那么就要重写equals方法了。
      • 1.2     equals
        • 默认的equals:

        • 这是java中默认的equals(obj) 方法,判断是不是同一个对象。
        • String重写判断流程为:用于判断两个对象内容是否相同;
          • 1.若A==B 即是同一个String对象 返回true(同一个对象,那么对象的内容也是相同的)
          • 2.若对比对象是String类型则继续,否则返回false
          • 3.判断A、B长度是否一样,不一样的话返回false
          • 4.逐个字符比较,若有不相等字符,返回false
      • 1.3   hashcode():是一个对象的 消息摘要函数,一种 压缩映射,其一般与equals()方法同时重写;若不重写hashCode方法,默认使用Object类的hashCode方法,该方法是一个本地方法,由 Object 类定义的 hashCode 方法会针对不同的对象返回不同的整数。当一个对象类型作为集合对象的元素时,那么这个对象应该拥有自己的equals()和hashCode();在使用集合set时,若向其加入两个相同(equals返回为true)的对象,由于hashCode函数没有进行重写,那么这两个对象的hashCode值必然不同,它们很有可能被分散到不同的桶中,容易造成重复对象的存在。
      • 1.4  hashcode() 和 equals() 的区别
        • 1、如果两个对象equals,Java运行时环境会认为他们的hashcode一定相等。
        • 2、如果两个对象不equals,他们的hashcode有可能相等。
        • 3、如果两个对象hashcode相等,他们不一定equals。
        • 4、如果两个对象hashcode不相等,他们一定不equals。
    • 2.wait(),wait(long),wait(long,int),notify(),notifyAll()
      • 在使用的时候要求在synchronize语句中使用
      • wait()用于让当前线程失去操作权限,当前线程进入等待序列
      • notify()用于随机通知一个持有对象的锁的线程获取操作权限
      • notifyAll()用于通知所有持有对象的锁的线程获取操作权限
      • wait(long) 和wait(long,int)用于设定下一次获取锁的距离当前释放锁的时间间隔
    • 3.toString()和getClass()
      • toString()返回一个String对象,用来标识自己
      • getClass()返回一个Class对象。
    • 4.clone()
      • 其实是使用了原型模式,
        • 1)在派生类中实现Cloneable借口。
        • 2)为了获取对象的一份拷贝,我们可以利用Object类的clone方法。
        • 3)在派生类中覆盖积累的clone方法,声明为public。
        • 4)在派生类的clone方法中,调用super.clone()。
    • 5.finalize()用于在垃圾回收
    • 6.registerNatives() 本地注册
      • native修饰的方法表示本地方法(跟系统有关,也可以理解为这个方法不是在java中实现的),据说这个方法在一个名为java.dll的动态库文件中。Object类中第40行(我的是jdk1.8)开始的 static{ registerNatives(); } 表示的是在类被加载时,调用 registerNatives()方法进行一些跟系统有关的方法调用,而这个方法的实现就在java.dll中(里面会根据不同系统来执行不同的底层操作)。
  • hashCode和equals函数用来判断对象是否相同 
    • 1.1     ==
      • 基本数据类型:(==),比较的是他们的值。
      • 引用类型(类、接口、数组)   :用(==)进行比较的时候,比较的是他们在内存中的存放地址,所以,除非是同一个new出来的对象,他们的比较后的结果为true,否则比较后结果为false。对象是放堆中的栈中存放的是对象的引用(地址)。由此可见'=='是对栈中的值进行比较的。如果要比较堆中对象的内容是否相同,那么就要重写equals方法了。
    • 1.2     equals
      • 默认的equals:

      • 这是java中默认的equals(obj) 方法,判断是不是同一个对象。
      • String重写判断流程为:用于判断两个对象内容是否相同;
        • 1.若A==B 即是同一个String对象 返回true(同一个对象,那么对象的内容也是相同的)
        • 2.若对比对象是String类型则继续,否则返回false
        • 3.判断A、B长度是否一样,不一样的话返回false
        • 4.逐个字符比较,若有不相等字符,返回false
    • 1.3   hashcode():是一个对象的 消息摘要函数,一种 压缩映射,其一般与equals()方法同时重写;若不重写hashCode方法,默认使用Object类的hashCode方法,该方法是一个本地方法,由 Object 类定义的 hashCode 方法会针对不同的对象返回不同的整数。当一个对象类型作为集合对象的元素时,那么这个对象应该拥有自己的equals()和hashCode();在使用集合set时,若向其加入两个相同(equals返回为true)的对象,由于hashCode函数没有进行重写,那么这两个对象的hashCode值必然不同,它们很有可能被分散到不同的桶中,容易造成重复对象的存在。
    • 1.4  hashcode() 和 equals() 的区别
      • 1、如果两个对象equals,Java运行时环境会认为他们的hashcode一定相等。
      • 2、如果两个对象不equals,他们的hashcode有可能相等。
      • 3、如果两个对象hashcode相等,他们不一定equals。
      • 4、如果两个对象hashcode不相等,他们一定不equals。
  • 不可变对象
    • 不可变对象(Immutable Objects)即对象一旦被创建它的状态(对象的数据,也即对象属性值)就不能改变,任何对它的改变都应该产生一个新的对象。JAVA平台类库中包含许多不可变类,如String、基本类型的包装类、BigInteger和BigDecimal等。
    •  确保类不能被继承:将类声明为final
    • 使用private和final修饰符来修饰该类的属性
      • 1)不要提供更改可变对象的方法
      • 2)不要共享对可变对象的引用,不要存储传给构造器的外部可变对象的引用。因为引用可变对象的成员变量和外部可变对象的引用指向同一块内存地址,用户可以在不可变类之外通过修改可变对象的值
    • 不要提供任何可以修改对象状态的方法(不仅仅是set方法, 还有任何其它可以改变状态的方法)
    • 总结:
      • 基本类型变量的值不可变;
      • 引用类型变量不能指向其他对象;
      • 引用类型所指向的对象的状态不可变;
      • 除了构造函数之外,不应该有其它任何函数(至少是任何public函数)修改任何成员变量;
  • final关键字
    • final关键字的基本用法在Java中,final关键字可以用来修饰类、方法和变量(包括成员变量和局部变量)。
      • 1、修饰类:当用final修饰一个类时,表明这个类不能被继承。也就是说,如果一个类你永远不会让他被继承,就可以用final进行修饰。final类中的成员变量可以根据需要设为final,但是要注意final类中的所有成员方法都会被隐式地指定为final方法。
      • 2、修饰方法: final修饰的方法表示此方法已经是“最后的、最终的”含义,亦即此方法不能被重写(可以重载多个final修饰的方法)。此处需要注意的一点是:因为重写的前提是子类可以从父类中继承此方法,如果父类中final修饰的方法同时访问控制权限为private,将会导致子类中不能直接继承到此方法,因此,此时可以在子类中定义相同的方法名和参数,此时不再产生重写与final的矛盾,而是在子类中重新定义了新的方法。(注:类的private方法会隐式地被指定为final方法。)
      • 3、修饰变量
        • final成员变量表示常量,只能被赋值一次,赋值后值不再改变。
        • 当final修饰一个基本数据类型时,表示该基本数据类型的值一旦在初始化后便不能发生变化;如果final修饰一个引用类型时,则在对其初始化之后便不能再让其指向其他对象了,但该引用所指向的对象的内容是可以发生变化的。本质上是一回事,因为引用的值是一个地址,final要求值,即地址的值不发生变化。
        • final修饰一个成员变量(属性),必须要显示初始化。这里有两种初始化方式,一种是在变量声明的时候初始化;第二种方法是在声明变量的时候不赋初值,但是要在这个变量所在的类的所有的构造函数中对这个变量赋初值。
        • 当函数的参数类型声明为final时,说明该参数是只读型的。即你可以读取使用该参数,但是无法改变该参数的值。
  • String(StringBuilder和StringBuffer)
    • String 定义与基础
      • String 的声明

      • String类表示字符串。所有字符串(字符串字面值)在Java程序中,如“abc”,实现这个类的实例。字符串常量(常量);它们的值在创建之后不能更改。支持可变字符串。因为字符串对象是不可变的,他们可以共享(享元模式)。
      • Java语言为字符串连接操作符(+)和将其他对象转换为字符串提供了特殊的支持。字符串连接实现通过StringBuilder (JDK1.5以后)或StringBuffer (JDK1.5以前)类和它的附加方法。字符串转换(转化为字符串)是通过实现toString方法,定义为类对象,并继承了所有Java类。
    • String 的不可变性
      • 区分引用和对象
        • 首先创建一个String对象s,然后让s的值为“ABCabc”, 然后又让s的值为“123456”。 从打印结果可以看出,s的值确实改变了。那么怎么还说String对象是不可变的呢? 其实这里存在一个误区: s 只是一个String对象的引用,并不是对象本身。对象在内存中是一块内存区,成员变量越多,这块内存区占的空间越大。引用只是一个 4 字节的数据,里面存放了它所指向的对象的地址,通过这个地址可以访问对象。 也就是说,s只是一个引用,它指向了一个具体的对象,当s=“123456”; 这句代码执行过之后,又创建了一个新的对象“123456”, 而引用s重新指向了这个新的对象,原来的对象“ABCabc”还在内存中存在,并没有改变。
      • String不可变:JDK1.7中String类的主要成员变量就剩下了两个:private final char value[]; private int hash;
        • String类其实就是对字符数组的封装。JDK6中, value是String封装的数组,offset是String在这个value数组中的起始位置,count是String所占的字符的个数。在JDK7中,只有一个value变量,也就是value中的所有字符都是属于String这个对象的。这个改变不影响本文的讨论。 除此之外还有一个hash成员变量,是该String对象的哈希值的缓存,这个成员变量也和本文的讨论无关。在Java中,数组也是对象(可以参考我之前的文章java中数组的特性)。 所以value也只是一个引用,它指向一个真正的数组对象。其实执行了String s = “ABCabc”; 这句代码之后,真正的内存布局应该是这样的:

        • value,offset和count这三个变量都是 private 的,并且没有提供setValue,setOffset和setCount等公共方法来修改这些值,所以在String类的外部无法修改String。也就是说一旦初始化就不能修改, 并且在String类的外部不能访问这三个成员。此外,value,offset和count这三个变量都是final的, 也就是说在String类内部,一旦这三个值初始化了, 也不能被改变。所以,可以认为String对象是不可变的了。
        • 方法内部重新创建新的String对象,并且返回这个新的对象,原来的对象是不会被改变的。这也是为什么像replace, substring,toLowerCase等方法都存在返回值的原因。也是为什么像下面这样调用不会改变对象的值:

      • String对象真的不可变吗?
        • 从上文可知String的成员变量是 private final 的,也就是初始化之后不可改变。那么在这几个成员中, value比较特殊,因为他是一个引用变量,而不是真正的对象。value是final修饰的,也就是说final不能再指向其他数组对象,那么我能改变value指向的数组吗? 比如,将数组中的某个位置上的字符变为下划线“_”。 至少在我们自己写的普通代码中不能够做到,因为我们根本不能够访问到这个value引用,更不能通过这个引用去修改数组,那么,用什么方式可以访问私有成员呢? 没错,用反射,可以反射出String对象中的value属性, 进而改变通过获得的value引用改变数组的结构。
    • String 对象创建方式
      • 1. 字面值形式: JVM会自动根据字符串常量池中字符串的实际情况来决定是否创建新对象 (要么不创建,要么创建一个对象,关键要看常量池中有没有)

        • 该种方式先在栈中创建一个对String类的对象引用变量s,然后去查找 “abc”是否被保存在字符串常量池中。若”abc”已经被保存在字符串常量池中,则在字符串常量池中找到值为”abc”的对象,然后将s 指向这个对象; 否则,在 堆 中创建char数组 data,然后在 堆 中创建一个String对象object,它由 data 数组支持,紧接着这个String对象 object 被存放进字符串常量池,最后将 s 指向这个对象。
        • 执行第 1 行代码时,“kvill” 入池并被 s0 指向;执行第 2 行代码时,s1 从常量池查询到” kvill” 对象并直接指向它;所以,s0 和 s1 指向同一对象。 由于 ”kv” 和 ”ill” 都是字符串字面值,所以 s2 在编译期由编译器直接解析为 “kvill”,所以 s2 也是常量池中”kvill”的一个引用。 所以,我们得出 s0==s1==s2;

      • 2. 通过 new 创建字符串对象 : 一概在堆中创建新对象,无论字符串字面值是否相等,
        • 通过 new 操作产生一个字符串(“abc”)时,会先去常量池中查找是否有“abc”对象,如果没有,则创建一个此字符串对象并放入常量池中。然后,在堆中再创建“abc”对象,并返回该对象的地址。所以,对于 String str=new String(“abc”):如果常量池中原来没有”abc”,则会产生两个对象(一个在常量池中,一个在堆中);否则,产生一个对象。用 new String() 创建的字符串对象位于堆中,而不是常量池中。它们有自己独立的地址空间;

        • 例子中,s0 还是常量池中”kvill”的引用,s1 指向运行时创建的新对象”kvill”,二者指向不同的对象。对于s2,因为后半部分是 new String(“ill”),所以无法在编译期确定,在运行期会 new 一个 StringBuilder 对象, 并由 StringBuilder 的 append 方法连接并调用其 toString 方法返回一个新的 “kvill” 对象。此外,s3 的情形与 s2 一样,均含有编译期无法确定的元素。因此,以上四个 “kvill” 对象互不相同。StringBuilder 的 toString 为:

    • 字符串常量池
      • 1、字符串池
        • 字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价。JVM为了提高性能和减少内存开销,在实例化字符串字面值的时候进行了一些优化。为了减少在JVM中创建的字符串的数量,字符串类维护了一个字符串常量池,每当以字面值形式创建一个字符串时,JVM会首先检查字符串常量池:如果字符串已经存在池中,就返回池中的实例引用;如果字符串不在池中,就会实例化一个字符串并放到池中。Java能够进行这样的优化是因为字符串是不可 变的,可以不用担心数据冲突进行共享。
      • 2、手动入池
        • 一个初始为空的字符串池,它由类 String 私有地维护。 当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串(用 equals(Object) 方法确定),则返回池中的字符串。否则,将此 String 对象添加到池中,并返回此 String 对象的引用。

      • 3、实例
        • 我们看这个例子,局部变量 str2,str3 指向字符串常量池中的两个对象。在运行时,第三行代码(str2+str3)实质上会被分解成五个步骤,分别是:

          • (1). 调用 String 类的静态方法 String.valueOf() 将 str2 转换为字符串表示;
          • (2). JVM 在堆中创建一个 StringBuilder对象,同时用str2指向转换后的字符串对象进行初始化;
          • (3). 调用StringBuilder对象的append方法完成与str3所指向的字符串对象的合并;
          • (4). 调用 StringBuilder 的 toString() 方法在堆中创建一个 String对象;
          • (5). 将刚刚生成的String对象的堆地址存赋给局部变量引用str4。
          • 而引用str5指向的是字符串常量池中字面值”abcd”所对应的字符串对象。由上面的内容我们可以知道,引用str4和str5指向的对象的地址必定不一样。这时,内存中实际上会存在五个字符串对象: 三个在字符串常量池中的String对象、一个在堆中的String对象和一个在堆中的StringBuilder对象。
        • 4) 情景四:字符串的编译期优化
          • Java 编译器对于类似“常量+字面值”的组合,其值在编译的时候就能够被确定了。在这里,str1 和 str9 的值在编译时就可以被确定,因此它们分别等价于: String str1 = “abcd”; 和 String str9 = “abcd”;Java 编译器对于含有 “String引用”的组合,则在运行期会产生新的对象 (通过调用StringBuilder类的toString()方法),因此这个对象存储在堆中。

    • 三大字符串类 : String、StringBuilder 和 StringBuffer
      • 1. String 与 StringBuilder
        • 简要的说, String 类型 和 StringBuilder 类型的主要性能区别在于 String 是不可变的对象。 事实上,在对 String 类型进行“改变”时,实质上等同于生成了一个新的 String 对象,然后将指针指向新的 String 对象。由于频繁的生成对象会对系统性能产生影响,特别是当内存中没有引用指向的对象多了以后,JVM 的垃圾回收器就会开始工作,继而会影响到程序的执行效率。所以,对于经常改变内容的字符串,最好不要声明为 String 类型。但如果我们使用的是 StringBuilder 类,那么情形就不一样了。因为,我们的每次修改都是针对 StringBuilder 对象本身的,而不会像对String操作那样去生成新的对象并重新给变量引用赋值。所以,在一般情况下,推荐使用 StringBuilder ,特别是字符串对象经常改变的情况下。
        • 在某些特别情况下,String 对象的字符串拼接可以直接被JVM 在编译期确定下来,这时,StringBuilder 在速度上就不占任何优势了。
        • 因此,在绝大部分情况下, 在效率方面:StringBuilder > String .
      • 2.StringBuffer 与 StringBuilder
        • 首先需要明确的是,StringBuffer 始于 JDK 1.0,而 StringBuilder 始于 JDK 5.0;此外,从 JDK 1.5 开始,对含有字符串变量 (非字符串字面值) 的连接操作(+),JVM 内部是采用 StringBuilder 来实现的,而在这之前,这个操作是采用 StringBuffer 实现的。
        • JDK的实现中 StringBuffer 与 StringBuilder 都继承自 AbstractStringBuilder。AbstractStringBuilder的实现原理为:AbstractStringBuilder中采用一个 char数组 来保存需要append的字符串,char数组有一个初始大小,当append的字符串长度超过当前char数组容量时,则对char数组进行动态扩展,即重新申请一段更大的内存空间,然后将当前char数组拷贝到新的位置,因为重新分配内存并拷贝的开销比较大,所以每次重新申请内存空间都是采用申请大于当前需要的内存空间的方式,这里是 2 倍。
        • StringBuffer 和 StringBuilder 都是可变的字符序列,但是二者最大的一个不同点是:StringBuffer类是线程安全的,他的方法都是synchronized关键字修饰的,而 StringBuilder 则不是。StringBuilder 提供的API与StringBuffer的API是完全兼容的,即,StringBuffer 与 StringBuilder 中的方法和功能完全是等价的,但是后者一般要比前者快。因此,可以这么说,StringBuilder 的提出就是为了在单线程环境下替换 StringBuffer 。
        • 在单线程环境下,优先使用 StringBuilder。三者都是final的,不允许被继承;
      • 3.实例
        • 1)

        • 2)每做一次 字符串连接操作 “+” 就产生一个 StringBuilder 对象,然后 append 后就扔掉。下次循环再到达时,再重新 new 一个 StringBuilder 对象,然后 append 字符串,如此循环直至结束。事实上,如果我们直接采用 StringBuilder 对象进行 append 的话,我们可以节省 N - 1 次创建和销毁对象的时间。所以,对于在循环中要进行字符串连接的应用,一般都是用StringBulider对象来进行append操作。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值