Java基础知识整理
String、StringBuilder、StringBuffer相关
-
String、StringBuilder、StringBuffer有什么区别与联系?
- 三兄弟的底层实现,jdk9之前是一个final修饰的char[]数组来保存字符,字符串的每个字符占2个字节。而jdk9之后采用final修饰的byte[]数组外加一个encoding-flag字段来保存字符,因此每个字符只占一个字节。从而使得字符串更加节省空间
- String为不可变的字符序列,每次对String进行操作都是生成新的String对象,这样效率低下且十分占内存空间。其是线程安全的
- StringBuilder和StringBuffer都继承于AbstractStringBuilder,它两都是可变的字符序列,其中StringBuilder线程不安全,执行速度较快。而StringBuffer线程安全,里面的方法都添加了synchronized关键字,执行速度相对于StringBuilder慢。
- 相比String,StringBuilder和StringBuffer类的对象能够被多次修改且不会生成新的未使用对象。其中在对字符串进行大量操作的时候,根据是否多线程去选择StringBuffer还是StringBuilder。
-
String是final类型的,不可变,但是通过反射可以暴力修改。
- 为了实现字符串池:String对象是缓存在字符串池中的,因此这些缓存的字符串是可以被多个客户端访问的,如果一个客户端的访问影响了别的客户端的行为,这样就存在风险。intern方法的调用:一个初始化为空的字符串池,它由String独自维护,当调用intern方法时,如果池已经包含了一个等于此String对象的字符串(equals)则返回池中的字符串,否则将此String对象添加到池中,并返回此String对象的引用。
- 防止通过继承String类,覆盖父类的方法。一旦继承将会破坏String的不可变性、缓存性以及hascode的计算方式。
- 为了线程安全:同一个字符串实例可以被多个线程共享,这样便不用因为线程安全问题而使用同步。
- 为了实现String可以创建hashCode不可变性:其创建的时候hashCode就被缓存了,不需要重新计算,这样使得字符串很适合作为HashMap中的键,字符串的处理速度要快于其他的对象。
- 确保数据安全性:例如用户名密码等。
-
Java语言规范对String做了如下说明:
- 每一个字符串常量都指向字符串池中或者堆内存中的一个字符串实例。
- 字符串对象值是固定的,一旦创建就不能再修改。
- 字符串常量或者常量表达式中的字符串都被使用方法String.intern()在字符串池中保留了唯一的实例。
-
String a = new String(“a”); 创建了两个对象或者一个
- 两个对象:在Java堆中保存一个a对象,且在常量池中也有一个"a"。
- 一个对象:常量池中已有"a",jvm将这个"a"直接赋给当前引用,不会在常量池中创建新的。
-
Java中的常量优化会使当前已知的字面量"a" + “b” + “c” 优化为"abc",而不是在常量池创建三个对象。
-
而类似这种下面的情况,则不会这么做,此时的s3指向的是堆中的字符串对象。
-
==与equals与hashCode
-
==:判断两个变量或实例是否指向同一个内存空间,即判断两个对象(的引用)是否是同一个。基本数据比较的是值,引用数据比较的是内存地址
-
equals:判断两个对象的内容是否相等,equals是判断两个变量或实例所指向的内存空间的值是否相同。或者字符串的内容是否相同
-
String中重写了equals来让其去比较对象的值,倒不如说一般都建议重写equals,因为底层源码中,equals就是==
-
public boolean equals(Object obj) { return (this == obj); }
-
-
hashCode与equals和==的一些相关规定
- 两个对象相等,hashCode一定相同,==和equals返回一定为true。
- 两个对象的hashCode相等,他们不一定相等。
- 两个对象的hashCode不相等,他们一定不是同一个对象。
-
hashCode通用规定
-
一个程序应用在执行期间,只要对象的equals方法的比较所用到的信息没有被修改,那么对同一对象的多次调用hashCode应该返回同一个值。
-
两个对象的equals返回true,则要求调用两个对象的hashCode必须返回同一个结果。
-
两个对象的equals返回false,则不要求两个对象一定不等(重写equals的情况)。
-
这也是为什么重写equals必须重写hashCode,如果不重写的话,会有一种情况(在一些场景下我们要求equals返回true,他们的实际值相等,可他们并不是同一个对象。hashCode默认是根据对象的内存地址经过hash算法得来的,每个对象在堆中的存储不一样,那么hashCode一定不相等,违反了前面的第二条)。
-
序列化与反序列化
- 序列化:把一个对象以字节流的形式保存到磁盘中,还有一个用途是在网络上传送对象的字节序列。序列化还可以用来实现深拷贝
- 反序列化:把一个字节流恢复为原来的对象。其读取的是Java对象中的数据,而不是Java类,所以反序列化要求必须提供该Java对象所属类的class文件。
- 要想实现序列化,则必须实现Serializable接口或者Externalizable接口,如果希望某个属性不被序列化,可以加上transient关键字
- 序列化的注意事项:
- 如果一个子类可序列化,则它的父类(直接或间接)要么含有无参构造器,要么也是可序列化的。
- 如果一个类的成员变量的类型除了基本类型和String类型,它还含有引用类型,则该引用类型必须是可序列化的,否则这个类就不是可序列化的。
- 在序列化的时候,对象的类名、实例变量(基本类型、String、数组、引用)都会被序列化;而方法、类变量(static)、transient修饰实例变量(也被称为瞬态实例变量)不会被序列化,在transient反序列化后,会出现0值和null值。
- 序列化ID的作用:Java序列化机制通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化的时候,JVM会把传来的字节流中的serialVersionID与本地相应实体类的serialVersionID进行比较,如果相同就认为是一致的,可以进行反序列化,否则抛出序列化版本不一致的异常。
深拷贝和浅拷贝
- 对象拷贝是将一个对象的属性拷贝到另一个有着相同类型的对象中去。调用clone方法可以实现拷贝对象,克隆对象要求必须实现Clonnable接口。拷贝对象是Java中一种创建新对象的方式。
- 浅拷贝:按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是这个基本类型的值;如果属性是引用类型,拷贝其引用但不拷贝引用的对象。因此如果其中一个对象的内存地址改变,另一个也会受到影响。从Object中继承而来的clone默认是浅拷贝。
- 深拷贝:拷贝所有属性,并给所有引用属性开辟新的内存空间,而不仅仅是拷贝引用。当对象和它所引用的对象一起拷贝时就发生深拷贝。深拷贝相比浅拷贝速度慢且花销大。实现深拷贝必须实现Clonnable接口和重写clone方法,且将该对象中引用的其他对象都要clone一份,这又要求引用其他的对象实现clonnable并重写clone方法。这么看来如果不停的引用,则这个将无穷无尽。所以实现彻底深拷贝是不可能的。
创建对象的方式
- 调用关键字new,最常见的方式,调用了构造函数。
- 使用反射机制,利用Class类的newInstance方法或者Constructor的newInstance方法,调用了构造函数。框架的设计大量使用了反射机制。
- 调用Object的clone方法,没有调用构造函数。
- 反序列化,我们进行反序列化的时候,JVM会给我们创建一个单独的对象,没有调用构造函数。
获取Class对象的方式
-
使用Class.forName(String className)方法,传入某个类的全限定类名,例如JDBC。只能在运行期确认该类是否存在。
-
调用某个类的.class属性来返回对应的class对象。代码安全,程序在编译阶段就可以检查需要访问的Class是否存在,程序性能好。
Class<ClassName> clazz = ClassName.class;
-
调用getClass()方法。
Java反射机制
-
反射:对于任意一个类,都能够知道这个类的所有属性和方法(包括静态属性和静态方法);对于任意一个对象,都能够调用它的任意一个方法和属性。即动态获取对象信息和调用对象方法的功能。反射还可以新建类的实例(对象)。
-
反射的优点:
- 极大的提高了程序的灵活性和扩展性,降低模块的耦合性
- 让程序创建和控制任何类的对象,无须提前硬编码目标类
- 可以在运行期构造一个类的对象,判断一个类所具有的的成员变量和方法,调用一个类的方法
- 框架技术的基础
-
缺点:
- 性能问题:反射机制包含了一些动态类型,故Java虚拟机不能够对这些动态代码进行优化
- 安全问题:反射技术要求程序必须在一个没有安全限制的环境中运行
- 程序健壮性问题:反射允许代码执行一些不被允许的操作,这样破坏了Java的程序结构的抽象性
-
反射的具体应用场景:
- JDBC中的Class.forName(“com.mysql.cj.jdbc.Driver”);
- Spring用反射来创建对象,根据XML配置文件信息来装载Bean,配置文件中读取的只是某个类的字符串类名,程序需要根据该字符串来创建对象
- 将程序内所有的XML或者properties配置文件加载入内存
- 在类里面解析XML或properties的内容,得到对应实体类的字节码字符串以及相关的属性信息
- 使用反射机制根据字符串获取某个类的Class实例
- 动态配置实例的属性
-
使用就是获取某个类的class对象,然后利用该对象下的各种getXxx方法即可获取。
Java的8种基本类型详解
byte | char | short | int | long | float | double | boolean | |
---|---|---|---|---|---|---|---|---|
表示范围 | -2^7 到 2^7-1 | / | -2^15 到 2^15-1 | -2^31 到 2^31-1 | -2^63 到 2^63-1 | / | / | / |
所占字节 | 1 | 2 | 2 | 4 | 8 | 4 | 8 | 1 |
其中对于int,32 64位的区别就是在对象头上: 32位系统上占用8bytes,64位系统上占用16bytes;
面试题:float是怎么存储的?
- 浮点数在计算机中占四个字节,一个浮点数由2部分组成,底数m和指数e,表示为±m × 2e,其中m和e都是二进制。其存储遵循IEEE - 754标准
- 底数部分占用一个24bit的值,其最高位始终为1,所以省去不存储,实际存储底数只有23bit。
- 指数部分占用8bit,可表示0 - 255,由于指数可正可负,所以此处算出的次方减去127才是真正的指数。
- 例如18.375的二进制为10010.011,将小数点左移四位得到1.0010011x 2^4,4+127=131,则存储的方式是:符号位0 一位,指数位10000011八位,尾数位0010011二十三位,不够补0。
面试题:char能否存储汉字?
- 因为java中的char是两个字节的,所有可以用来存储中文(一个中文也是两个字节),而在c语言中char只是一个字节,所以不能用来存储中文,要想存储中文,只能用字节数组。
- char是按照字符存储的,不管英文还是中文,固定占用占用2个字节,用来储存Unicode字符。范围在0-65536。unicode编码字符集中包含了汉字,所以,char型变量中当然可以存储汉字啦。不过,如果某个特殊的汉字没有被包含在unicode编码字符集中,那么,这个char型变量中就不能存储这个特殊汉字。
包装类相关
-
装箱:将基本类型用它们对应的引用类型来包装,例如int->Integer
-
拆箱:将包装类型转换为基本数据类型
-
自动装箱:进行创建的时候 Integer a = 127; 就将127这个基本类型自动装箱成Integer。
-
自动拆箱:在Integer类型与int进行比较时,会把Integer自动拆箱成int类型进行比较。
Integer a = 128; //将127这个基本类型自动装箱成Integer Integer b = 128; //将127这个基本类型自动装箱成Integer int c = 128; System.out.println(a == b); //不在在-128到127这个范围,会创建新的对象,所以a != b System.out.println(a.equals(b)); //比较对象的值,直接就true System.out.println(a == c); //将a自动拆箱成int的128和c进行比较,返回true
意义:Java是一种完全面向对象的语言,但是对于CPU来说,处理一个完整的对象需要很多指令,并且又需要很多内存。于是Java有一种机制,使得基本类型在一般的编程中被当做非对象的简单类型处理,另一些场合又允许他们是个对象,例如方法传值需要传递一个Object类型,还有就是泛型指定的时候。
面向对象相关
- 面向对象:无关底层,降低了程序之间的耦合性,可维护性好。抽象出一个类,里面有数据也有解决问题的方法,需要什么功能就直接用,不必一步一步实现。
- 面向过程:具体化,流程化,解决一个问题需要一步一步分析,一步一步实现。性能要优于面向对象。
- 面向对象三大特性:
- 封装:隐藏对象的属性和实现细节,仅仅对外提供公共访问的方式,提高复用性和安全性。
- 继承