数据类型,包装类

数据类型

常量

1
2
3
4
5
6
7
8
9
10
//输出整数常量
System.out.println(123);
//输出小数常量
System.out.println(0.125);
//输出字符常量
System.out.println('A');
//输出布尔常量
System.out.println(true);
//输出字符串常量
System.out.println("你好Java");

Java的数据类型分为两大类

Java 中有 8 种基本数据类型,分别为:

  • 6 种数字类型:
    • 4 种整数型:byteshortintlong
    • 2 种浮点型:floatdouble
  • 1 种字符类型:char
  • 1 种布尔型:boolean

这 8 种基本数据类型的默认值以及所占空间的大小如下:

基本类型 位数 字节 默认值 取值范围
byte 8 1 0 -128 ~ 127
short 16 2 0 -32768(-2^15) ~ 32767(2^15 - 1)
int 32 4 0 -2147483648 ~ 2147483647
long 64 8 0L -9223372036854775808(-2^63) ~ 9223372036854775807(2^63 -1)
char 16 2 ‘u0000’ 0 ~ 65535(2^16 - 1)
float 32 4 0f 1.4E-45 ~ 3.4028235E38
double 64 8 0d 4.9E-324 ~ 1.7976931348623157E308
boolean 1 false true、false

char:Unicode编码的字符,或字符的整数编码,必须用单引号
float:默认值是0.0f;
double:默认值是0.0d;

基本类型字面值规则

  • 整数字面值是int类型,如果右侧赋值超出int范围,需要做转型处理
  • byte,short,char 三种比int小的整数,在自身范围内可以直接赋值。byte d=1+3 正确,1+3编译器会自动转成4
  • 浮点数字面值是double;浮点数转成整数会直接舍弃小数点后位数。
  • 字面值后缀,L长整型 D双精度浮点型 F单精度浮点型
  • 字面值前缀,0b 二进制;0x 十六进制;0 八进制; \u char 类型十六进制

基本类型的类型转换

转换规则
范围小的类型向范围大的类型提升, byte、short、char 运算时直接提升为 int 。
byte、short、char‐‐>int‐‐>long‐‐>float‐‐>double
自动转换

1
2
3
int i = 1;
double d = 2.5; //int类型和double类型运算,结果是double类型
double e = d+i; //int类型会提升为double类型

强制类型转换
将取值范围大的类型强制转换成取值范围小的类型
比较而言,自动转换是Java自动执行的,而强制转换需要我们自己手动执行

1
2
3
4
5
6
7
short s = 1;
/*
出现编译失败,s和1做运算的时候,1是int类型,s会被提升为int类型,s+1后的结果是int类型,将结果在赋值会short类型时发生错误
short内存2个字节,int类型4个字节,必须将int强制转成short才能完成赋值
*/
s = s + 1//编译失败
s = (short)(s+1);//编译成功

类型互转会出现什么问题吗?

**数据丢失:**当将一个范围较大的数据类型转换为一个范围较小的数据类型时,可能会发生数据丢失。例如,将一个 long 类型的值转换为 int 类型时,如果 long 值超出了 int 类型的范围,转换结果将是截断后的低位部分,高位部分的数据将丢失。
**数据溢出:**与数据丢失相反,当将一个范围较小的数据类型转换为一个范围较大的数据类型时,可能会发生数据溢出。例如,将一个 int 类型的值转换为 long类型时,转换结果会填充额外的高位空间,但原始数据仍然保持不变。

**精度损失:**在进行浮点数类型的转换时,可能会发生精度损失。由于浮点数的表示方式不同,将一个单精度浮点数( float )转换为双精度浮点数( double )时,精度可能会损失。

**类型不匹配导致的错误:**在进行类型转换时,需要确保源类型和目标类型是兼容的。如果两者不兼容会导致编译错误或运行时错误。

为什么浮点数运算的时候会有精度丢失的风险?

浮点数运算精度丢失代码演示:

1
2
3
4
5
float a = 2.0f - 1.9f;
float b = 1.8f - 1.7f;
System.out.println(a);// 0.100000024
System.out.println(b);// 0.099999905
System.out.println(a == b);// false

为什么会出现这个问题呢?

这个和计算机保存浮点数的机制有很大关系。我们知道计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。这也就是解释了为什么浮点数没有办法用二进制精确表示。

就比如说十进制下的 0.2 就没办法精确转换成二进制小数:

1
2
3
4
5
6
7
// 0.2 转换为二进制数的过程为,不断乘以 2,直到不存在小数为止,
// 在这个计算过程中,得到的整数部分从上到下排列就是二进制的结果。
0.2 * 2 = 0.4 -> 0
0.4 * 2 = 0.8 -> 0
0.8 * 2 = 1.6 -> 1
0.6 * 2 = 1.2 -> 1
0.2 * 2 = 0.4 -> 0(发生循环)
image-20240329165501076

如何解决浮点数运算的精度丢失问题?

BigDecimal 可以实现对浮点数的运算,不会造成精度丢失。通常情况下,大部分需要浮点数精确运算结果的业务场景(比如涉及到钱的场景)都是通过 BigDecimal 来做的。

1
2
3
4
5
6
7
8
9
10
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
BigDecimal c = new BigDecimal("0.8");

BigDecimal x = a.subtract(b);
BigDecimal y = b.subtract(c);

System.out.println(x); /* 0.1 */
System.out.println(y); /* 0.1 */
System.out.println(Objects.equals(x, y)); /* true */

BigDecimal 类的底层实现主要依赖于整数数组来表示十进制数。每个数组元素都包含一段数字,通常是一个或多个十进制位。这种表示方式允许 BigDecimal 类可以处理任意长度的数值,并且能够保持精度。

超过 long 整型的数据应该如何表示?

基本数值类型都有一个表达范围,如果超过这个范围就会有数值溢出的风险。

在 Java 中,64 位 long 整型是最大的整数类型。

1
2
3
long l = Long.MAX_VALUE;
System.out.println(l + 1); // -9223372036854775808
System.out.println(l + 1 == Long.MIN_VALUE); // true

BigInteger 内部使用 int[] 数组来存储任意大小的整形数据。

相对于常规整数类型的运算来说,BigInteger 运算的效率会相对较低。

包装类

Java提供了两个类型系统,基本类型与引用类型,使用基本类型在于效率,然而很多情况,会创建对象使用,因为对象可以做更多的功能,如果想要我们的基本类型像对象一样操作,就可以使用基本类型对应的包装类,如下:
基本类型–>包装类(引用类型,包装类都位于java.lang包下)

byte Byte
short Short
int Integer 【特殊】
long Long
float Float
double Double
char Character 【特殊】
boolean Boolean

装箱与拆箱

基本类型与对应的包装类对象之间,来回转换的过程称为”装箱“与”拆箱“

装箱: 基本数值—->包装对象(装箱)

构造方法
Integer(int value) :构造一个新分配的 Integer 对象,它表示指定的 int 值
Integer(String s) :构造一个新分配的 Integer 对象,它表示 String 参数所指示的 int 值。传递的字符串,必须是基本类型的字符串,否则会抛出异常 “100” 正确 “a” 抛异常

静态方法

static Integer valueOf(int i):返回一个表示指定的 int 值的 Integer 实例
static Integer valueOf(String s):返回保存指定的 String 的值的 Integer 对象

1
2
Integer i = new Integer(4);//使用构造函数函数
Integer i = Integer.valueOf(4);//使用包装类中的valueOf方法

拆箱: 包装对象—->基本数值(拆箱)

int intValue() :以 int 类型返回该 Integer 的值。

1
2
Integer i = new Integer(1);
int num = i.intValue(); //把包装类变为基本类型

自动装箱与自动拆箱

由于我们经常要做基本类型与包装类之间的转换,从Java 5(JDK 1.5)开始,基本类型与包装类的装箱、拆箱动作可以自动完成。例如:

1
2
3
4
5
6
7
8
//自动装箱,相当于Integer i = Integer.valueOf(4);
Integer i = 4;

//将i对象转成基本数值(自动拆箱)
i.intValue() + 5;

//加法运算完成后,再次装箱,把基本数值转成对象。
i = i + 5;

装箱其实就是调用了 包装类的valueOf()方法,拆箱其实就是调用了 xxxValue()方法。

因此,

  • Integer i = 10 等价于 Integer i = Integer.valueOf(10)
  • int n = i 等价于 int n = i.intValue();

注意:如果频繁拆装箱的话,也会严重影响系统的性能。我们应该尽量避免不必要的拆装箱操作。

包装类型的缓存机制

Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。

Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character 创建了数值在 [0,127] 范围的缓存数据,Boolean 直接返回 True or False

Integer 缓存源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
private static class IntegerCache {
static final int low = -128;
static final int high;
static {
// high value may be configured by property
int h = 127;
}
}

Character 缓存源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static Character valueOf(char c) {
if (c <= 127) { // must cache
return CharacterCache.cache[(int)c];
}
return new Character(c);
}

private static class CharacterCache {
private CharacterCache(){}
static final Character cache[] = new Character[127 + 1];
static {
for (int i = 0; i < cache.length; i++)
cache[i] = new Character((char)i);
}
}

Boolean 缓存源码:

1
2
3
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}

如果超出对应范围仍然会去创建新的对象,缓存的范围区间的大小只是在性能和资源之间的权衡。

两种浮点数类型的包装类 Float,Double 并没有实现缓存机制。

下面我们来看一个问题:下面的代码的输出结果是 true 还是 false 呢?

1
2
3
Integer i1 = 40;
Integer i2 = new Integer(40);
System.out.println(i1==i2);

Integer i1=40 这一行代码会发生装箱,也就是说这行代码等价于 Integer i1=Integer.valueOf(40) 。因此,i1 直接使用的是缓存中的对象。而Integer i2 = new Integer(40) 会直接创建新的对象。

因此,答案是 false

记住:所有整型包装类对象之间值的比较,全部使用 equals 方法比较

引用类型

实际的开发中,引用类型的使用非常重要,也是非常普遍的。我们可以在理解基本类型的使用方式基础上,进一步 去掌握引用类型的使用方式。基本类型可以作为成员变量、作为方法的参数、作为方法的返回值,那么当然引用类型也是可以的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// 游戏当中的英雄角色类
public class Hero {
private String name; // 英雄的名字
private int age; // 英雄的年龄
private Weapon weapon; // 英雄的武器 引用类型


public Hero() {
}
public Hero(String name, int age, Weapon weapon) {
this.name = name;
this.age = age;
this.weapon = weapon;
}
public void attack() {
System.out.println("年龄为" + age + "的" + name + "用" + weapon.getCode() + "攻击敌方。");
}
public String getName() {return this.name;}
public void setName(String name) {this.name = name;}
public int getAge() {return this.age;}
public void setAge(int age) {this.age = age;}
public Weapon getWeapon() {return this.weapon;}
public void setWeapon(Weapon weapon) {this.weapon = weapon;}
}



//武器类
public class Weapon {
private String code; // 武器的代号
public Weapon() {
}
public Weapon(String code) {
this.code = code;
}
//省略getter setter方法
}


public class DemoMain {
public static void main(String[] args) {
// 创建一个英雄角色
Hero hero = new Hero();
// 为英雄起一个名字,并且设置年龄
hero.setName("盖伦");
hero.setAge(20);
// 创建一个武器对象
Weapon weapon = new Weapon("多兰剑");
// 为英雄配备武器
hero.setWeapon(weapon);
// 年龄为20的盖伦用多兰剑攻击敌方。
hero.attack();
}
}


interface作为成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class Hero {
private String name; // 英雄的名称
private Skill skill; // 英雄的技能
public Hero() {
}
public Hero(String name, Skill skill) {
this.name = name;
this.skill = skill;
}
public void attack() {
System.out.println("我叫" + name + ",开始施放技能:");
skill.use(); // 调用接口中的抽象方法
System.out.println("施放技能完成。");
}
public String getName() {return this.name;}
public void setName(String name) {this.name = name;}
public Skill getSkill() {return this.skill;}
public void setSkill(Skill skill) {this.skill = skill;
}


public interface Skill {
void use(); // 释放技能的抽象方法
}


public static void main(String[] args) {
Hero hero = new Hero();
hero.setName("艾希"); // 设置英雄的名称

// 设置英雄技能
// hero.setSkill(new SkillImpl()); // 使用单独定义的实现类


// 还可以改成使用匿名内部类
// Skill skill = new Skill() {
// @Override
// public void use() {
// System.out.println("Pia~pia~pia~");
// }
// };
// hero.setSkill(skill);


// 进一步简化,同时使用匿名内部类和匿名对象
hero.setSkill(new Skill() {
@Override
public void use() {
System.out.println("Biu~Pia~Biu~Pia~");
}
});
hero.attack();
}


interface作为方法参数和返回值类型
当接口作为方法的参数时,需要传递什么呢?当接口作为方法的返回值类型时,需要返回什么呢?对,其实都是它的 子类对象。 ArrayList 类我们并不陌生,查看API我们发现,实际上,它是java.util.List 接口的实现类。所以,当我们看见 List 接口作为参数或者返回值类型时,当然可以将 ArrayList 的对象进行传递或返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class DemoInterface {
public static void main(String[] args) {
// 左边是接口名称,右边是实现类名称,这就是多态写法
List<String> list = new ArrayList<>();
List<String> result = addNames(list);
for (int i = 0; i < result.size(); i++) {
System.out.println(result.get(i));
}
}


//参数和返回值都是接口的方法
public static List<String> addNames(List<String> list) {
list.add("迪丽热巴");
list.add("古力娜扎");
list.add("玛尔扎哈");
list.add("沙扬娜拉");
return list;
}
}

数据类型,包装类
http://example.com/2023/05/13/Java/Java基础/数据类型、包装类/
作者
PALE13
发布于
2023年5月13日
许可协议