bit(位):表示信息的最小单位,是二进制数的一位包含的信息;
byte(字节):用来计量存储容量的一种计量单位;
1 byte = 8 bit(1个字节等于8位);
基本类型 | 占据空间大小 | 取值范围 | 默认值 |
---|---|---|---|
布尔型——boolean | 不确定 | true/false | false |
字节型——byte | 1个字节 | -128~127 | 0 |
整型——int | 4个字节 | -2^31 ~ 2^31-1(-2147483648~2147483647) | 0 |
短整型——short | 2个字节 | -2^15 ~ 2^15-1(-32768~32767) | 0 |
长整型——long | 8个字节 | -2^63 ~ 2^63-1(-9223372036854775808 ~ 9223372036854775807) | 0 |
字符型——char | 2个字节 | 0~2^16-1(0 ~ 65535无符号) | \u0000 |
单精度浮点型——float | 4个字节 | -2^128 ~ 2^128 | 0.0F |
双精度浮点型——double | 8个字节 | -2^1024 ~ 2^1024 | 0.0D |
(1)带符号数(正数/负数)在计算机中存储方式?
原码:原码就是符号位加上真值的绝对值, 即用第一位表示符号, 其余位表示值。
补码:正数的补码就是其本身。负数的补码是在其原码的基础上, 符号位不变, 其余各位取反, 最后+1. (即在反码的基础上+1)
补码是计算机存储带符号数的方式,可以解决0的符号问题以及两个编码问题。
为什么byte类型(8位二进制表示)的取值范围为-128~127?
0用[0000 0000]表示,-128用[1000 0000]表示。byte类型1个字节,8位二进制,范围为[1000 0000]~[0111 1111]
(2)浮点数(小数)在计算机中存储方式?
(1)浮点型数据精度丢失的原因
将十进制浮点数转换为二进制浮点数时,小数的二进制有时也是不可能精确的。
就如同十进制不能准确表示1/3,二进制也无法准确表示1/10,而double类型存储尾数部分最多只能存储52位,于是,计算机在存储该浮点型数据时,便出现了精度丢失。
例:十进制小数如何转化为二进制数
算法是乘以2直到没有了小数为止。举个例子,0.9表示成二进制数
0.9*2=1.8 取整数部分 1
0.8(1.8的小数部分)*2=1.6 取整数部分 1
0.6*2=1.2 取整数部分 1
0.2*2=0.4 取整数部分 0
0.4*2=0.8 取整数部分 0
0.8*2=1.6 取整数部分 1
0.6*2=1.2 取整数部分 0
.........
0.9二进制表示为(从上往下): 1100100100100......
注意:上面的计算过程循环了,也就是说*2永远不可能消灭小数部分,这样算法将无限下去。很显然,小数的二进制表示有时是不可能精确的 。
因此将11.9化为二进制后大约是” 1011. 1110011001100110011001100…”。
(2)浮点型数据精度丢失的解决方法
商业运算中应用场景:例如某用户有10块钱,买了一件商品花了8.8,理应剩下1.2元。但却无法继续购买价格为1.2元的商品。
double d1 = 10;
double d2 = 8.8;
double c = d1 - d2;
System.out.println("d1 - d2 = "+c);
// 输出
d1 - d2 = 1.1999999999999993
//加法
public static BigDecimal add(double v1,double v2){
BigDecimal b1 = new BigDecimal(Double.toString(v1));//这里使用的是String构造器,将double转换为String类型
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.add(b2);
}
//减法
public static BigDecimal sub(double v1,double v2){
BigDecimal b1 = new BigDecimal(Double.toString(v1));//同上
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.subtract(b2);//这是b1-b2,可以理解为从b1截取b2
}
//乘法
public static BigDecimal mul(double v1,double v2){
BigDecimal b1 = new BigDecimal(Double.toString(v1));//同上
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.multiply(b2);
}
//除法
public static BigDecimal div(double v1,double v2){
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.divide(b2,2,BigDecimal.ROUND_HALF_UP); //四舍五入,保留2位小数,除不尽的情况
}
封装类(如Integer)是基本数据类型(如int)的包装类。
封装类(Integer) | 基本类型(int) | |
---|---|---|
存储数据 | 封装类本质是对象的引用,需实例化后使用。实际上是生成一个指向该对象的引用(存储对象地址) | 值(基本数据类型是一个变量,直接存放数值) |
属性和方法 | 封装类有属性和方法,利用这些方法和属性来处理数据,如Integer.parseInt(Strings) | 基本数据类型都是final修饰的,不能继承扩展新的类、新的方法 |
默认值 | null | 0 |
存储位置 | 封装类的对象引用存储在栈中,实际的对象存储在堆中 | 栈 |
使用场景 | 更好地处理数据之间的转换 | 速度快(不涉及对象的构造与回收) |
封装类(如Integer)是基本数据类型(如int)的包装类。装箱就是 自动将基本数据类型转换为包装器类型;拆箱就是 自动将包装器类型转换为基本数据类型。
Integer i = 10; // 实际上执行Integer.valueOf(10);
Integer i = 10; //装箱
int t = i; //拆箱,实际上执行了 int t = i.intValue();
在java中数组也是对象。因此,对象存放在内存中的原理同样适用于数组。
当创建一个数组时,在堆中会为数组对象分配一段内存空间,并返回一个引用。数组对象的引用存放在栈中,实际的数组对象存放在堆中。
多维数组在内存中存储方式:
使用Java一维数组,仿照ArrayList源码,封装相关构造、获取元素个数、容量大小、判空、增删查改等功能。
对Java一维数组E[]自定义ArrayList集合
下面是部分实现:
/**
* 通过对数组封装实现自己的Array类
*/
public class MyArrayList<E> {
private E[] data; // 定义一个整型的一维数组的成员变量
private int size; // 数组中元素个数
// 获取数组中元素的个数
public int getSize() {
return size;
}
// 判断数组是否为空
public boolean isEmpty() {
return size == 0;
}
// 向数组的第index位置插入元素e
public void add(int index, E e) {
if (index < 0 || index > size)
throw new IllegalArgumentException("Add failed. Require index >= 0 and index <= size.");
if (size - data.length >= 0) {
int newCapacity = data.length + (data.length >> 1); // 扩容1.5倍
resize(newCapacity);
}
for (int i = size - 1; i >= index; i--)
data[i + 1] = data[i];
data[index] = e;
size++;
}
// 动态数组扩容 newCapacity 扩容长度
private void resize(int newCapactity) {
E[] newData = (E[]) new Object[newCapactity];
for (int i = 0; i < size; i++) {
newData[i] = data[i];
}
data = newData;
newData = null;
}
}
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
}
在 Java 9 之后,String 类的实现改用 byte 数组存储字符串,同时使用 coder 来标识使用了哪种编码。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final byte[] value;
/** The identifier of the encoding used to encode the bytes in {@code value}. */
private final byte coder;
}
补充:不可变的实现:
String类被final修饰,保证类不被继承。
String内部所有成员都设置为私有变量,并且用final修饰符修饰,保证成员变量初始化后不被修改。
不提供setter方法改变成员变量,即避免外部通过其他接口修改String的值。
通过构造器初始化所有成员(value[])时,对传入对象进行深拷贝(deep copy),避免用户在String类以外通过改变这个对象的引用来改变其内部的值。
在getter方法中,不要直接返回对象引用,而时返回对象的深拷贝,防止对象外泄。
(二)不可变的好处
缺点:String对象不适用于经常发生修改的场景,会创建大量的String对象。
(三)String 的 “改变”?
public static void main(String[] args) {
String s = "ABCDEF";
System.out.println("s = " + s);
s = "123456";
System.out.println("s = " + s);
}
String的改变实际上是创建了一个新的String对象"123456",并将引用指向了这个新的对象,同时原来的String对象"ABCDEF"并没有发生改变,仍保存在内存中。
(四)String 的不可变 真的不可变?
通过反射获取value数组直接改变内存数组中的数据是可以修改所谓的"不可变"对象的。
public static void reflectString() throws Exception{
// 创建字符串"ABCDEF"并赋给引用s
String s = "ABCDEF";
System.out.println("s = " + s); // s = ABCDEF
Field valueField = s.getClass().getDeclaredField("value"); // 获取String类中value字段
valueField.setAccessible(true); // 改变value属性的访问权限
char[] value = (char[]) valueField.get(s); // 获取s对象上的value属性的值
value[0] = 'a'; // 改变value所引用的数组中的某个位置字符
value[2] = 'c';
value[4] = 'e';
System.out.println("s = " + s); // s = aBcDeF
}
String | StringBuffer | StringBuilder | |
可变性 | String | StringBuffer | StringBuilder |
线程安全 | 安全(不可变) | 安全(Synchronized) | 不安全 |
执行效率 | 高 | 低(Synchronized) | 高 |
适用场景 | 操作少量的数据,不需要频繁拼接 | 多线程操作大量数据 只有在对线程安全要求高的情况下使用StringBuffer |
单线程操作大量数据 |
备注 | 在字符串修改/拼接时,String是不可变的对象, 因此在每次对String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。不仅效率低下,还会大量浪费内存空间。 使用 StringBuffer/StringBuilder 类时,每次都会对 StringBuffer/StringBuilder 对象本身进行修改操作,而不产生新的未使用对象。 |
对于String,其对象的引用都是存储在栈中的。
java中对String对象特殊对待,所以在heap区域分成了两块,一块是字符串常量池(String constant pool),用于存储java字符串常量对象,另一块用于存储普通对象及字符串对象。
intern() 方法返回字符串对象的规范化表示形式,即一个字符串,内容与此字符串相同,但一定取自具有唯一字符串的池。
它遵循以下规则:对于任意两个字符串 s 和 t,当且仅当 s.equals(t) 为 true 时,s.intern() == t.intern() 才为 true。
String s1 = new String("aaa");
String s2 = new String("aaa");
System.out.println(s1 == s2); // false,指向堆内不同引用
String s3 = s1.intern();
String s4 = s1.intern();
System.out.println(s3 == s4); // true,指向字符串常量池中相同引用
String s5 = "bbb";
String s6 = "bbb";
System.out.println(s5 == s6); // true,指向字符串常量池中相同引用
// String d = "a"+"b"+"c";等效于
String d = new StringBuilder().append("a").append("b").append("c").toString();
但并不是说直接用“+”号拼接就可以达到StringBuilder的效率了,因为每次使用 "+"拼接 都会新建一个StringBuilder对象,并且最后toString()方法还会生成一个String对象。在循环拼接十万次的时候,就会生成十万个StringBuilder对象,会产生大量内存消耗。
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);
return new String(buf, true);
}
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;
}
他扩容的方法的代码如下,可见,当容量不够的时候,数组容量右移1位(也就是翻倍)再加2。
private int newCapacity(int minCapacity) {
// overflow-conscious code
int newCapacity = (value.length << 1) + 2;
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
? hugeCapacity(minCapacity)
: newCapacity;
}
拼接方式 | + | concat | StringBuilder/StringBuffer |
---|---|---|---|
原理 | jvm采用append优化,每次执行都会新建一个StringBuilder和String对象 | 申请一个char类型的buf数组,将需要拼接的字符串都放在这个数组里,最后再创建并返回一个新的String对象 | 利用char数组保存字符,对Stringbuilder/StringBuffer直接修改,不生成新的String对象 |
比较 | 最慢且效率最低,适用于书写方便场景 | 适用于少量字符串拼接(会新建String对象) | 适用于多个字符串拼接,当不考虑线程的情况下,StringBuilder效率比StringBuffer(Synchronized)高 |