ZetCode

Java 数据类型

上次修改时间:2024 年 7 月 16 日

在本文中,我们将展示如何在 Java 中使用数据类型。

计算机程序,包括电子表格、文本编辑器、计算器或聊天客户端,都使用数据。用于处理各种数据类型的工具是现代计算机语言的重要组成部分。数据类型是一组值以及允许对这些值执行的操作。

Java 编程语言是一种静态类型语言。这意味着每个变量和每个表达式都具有在编译时已知的类型。Java 语言也是一种强类型语言,因为类型限制了变量可以保存或表达式可以产生的值,限制了对这些值支持的操作,并确定了操作的含义。

强静态类型有助于在编译时检测错误。在像 Ruby 或 Python 这样的动态类型语言中,变量可以在一段时间内接收不同的数据类型。在 Java 中,一旦将变量声明为某种数据类型,它就不能保存其他数据类型的值。

Java 中有两种基本数据类型:原始类型引用类型。原始类型包括:

Java 中每种类型都有特定的关键字。原始类型不是 Java 中的对象。原始数据类型不能存储在仅适用于对象的 Java 集合中。它们可以放入数组中代替。

引用类型包括:

还有一个特殊的 null 类型,它表示一个不存在的值。

在 Ruby 编程语言中,一切都是对象。甚至基本数据类型也是如此。

#!/usr/bin/ruby

4.times { puts "Ruby" }

这个 Ruby 脚本将“Ruby”字符串四次打印到控制台。我们调用数字 4 的 times 方法。这个数字是 Ruby 中的一个对象。

Java 采用不同的方法。它具有原始数据类型和包装类。包装类将原始类型转换为对象。包装类将在下一章中介绍。

布尔值

我们的世界内置了一种二元性。有天堂和地球,水和火,阴和阳,男人和女人,爱和恨。在 Java 中,boolean 数据类型是一种原始数据类型,它具有两个值之一:truefalse

幸福的父母正在等待孩子的出生。他们为两种可能性都选择了名字。如果是男孩,他们选择了 Robert。如果是女孩,他们选择了 Victoria。

Main.java
import java.util.Random;

void main() {

    String name = "";
    Random r = new Random();
    boolean male = r.nextBoolean();

    if (male == true) {

        name = "Robert";
    }

    if (male == false) {

        name = "Victoria";
    }

    System.out.format("We will use name %s%n", name);

    System.out.println(9 > 8);
}

该程序使用随机数生成器来模拟我们的情况。

Random r = new Random();
boolean male = r.nextBoolean();

Random 类用于生成随机数。nextBoolean 方法随机返回一个布尔值。

if (male == true) {

    name = "Robert";
}

如果布尔变量 male 等于 true,我们将 name 变量设置为“Robert”。if 关键字与布尔值一起使用。

if (male == false) {

    name = "Victoria";
}

如果随机生成器选择 false,那么我们将 name 变量设置为“Victoria”。

System.out.println(9 > 8);

关系运算符会产生一个布尔值。此行将 true 打印到控制台。

$ java Main.java
We will use name Robert
true
$ java Main.java
We will use name Robert
true
$ java Main.java
We will use name Victoria
true

整数

整数是实数的子集。它们没有小数或小数部分。整数属于集合 Z = {..., -2, -1, 0, 1, 2, ...} 整数是无限的。

在计算机语言中,整数(通常)是原始数据类型。计算机实际上只能处理整数值的一个子集,因为计算机的容量是有限的。整数用于计数离散实体。我们可以有 3、4 或 6 个人,但不能有 3.33 个人。我们可以有 3.33 公斤、4.564 天或 0.4532 公里。

类型 大小 范围
byte 8 位 -128 到 127
short 16 位 -32,768 到 32,767
char 16 位 0 到 65,535
int 32 位 -2,147,483,648 到 2,147,483,647
long 64 位 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807
表:Java 中的整数类型

可以根据我们的需要使用这些整数类型。然后,我们可以使用 byte 类型来存储一个女人所生孩子的数量的变量。最长寿的验证者死于 122 岁,因此我们可能至少选择 short 类型作为年龄变量。这将为我们节省一些内存。

整数文字可以用十进制、十六进制、八进制或二进制表示法表示。如果一个数字具有 ASCII 字母 Ll 后缀,则它的类型为 long。否则,它的类型为 int。建议使用大写字母 L 来指定长数字,因为小写字母 l 很容易与数字 1 混淆。

int a = 34;
byte b = 120;
short c = 32000;
long d = 45000;
long e = 320000L;

我们有五个赋值。值 34、120、32000 和 45000 是类型为 int 的整数文字。没有 byteshort 类型的整数文字。如果这些值适合目标类型,则编译器不会抱怨并自动执行转换。对于小于 Integer.MAX_VALUElong 数字,L 后缀是可选的。

long x = 2147483648L;
long y = 2147483649L;

对于大于 Integer.MAX_VALUElong 数字,我们必须添加 L 后缀。

当我们使用整数时,我们处理的是离散项目。例如,我们可以使用整数来计算苹果的数量。

Main.java
void main() {

    int baskets = 16;
    int applesInBasket = 24;

    int total = baskets * applesInBasket;

    System.out.format("There are total of %d apples%n", total);
}

在我们的程序中,我们计算苹果的总数。我们使用乘法运算。

int baskets = 16;
int applesInBasket = 24;

篮子的数量和每个篮子里的苹果数量都是整数值。

int total = baskets * applesInBasket;

将这些值相乘,我们也会得到一个整数。

$ java Main.java
There are total of 384 apples

整数可以在 Java 中以四种不同的表示法指定:十进制、八进制、十六进制和二进制。二进制表示法是在 Java 7 中引入的。十进制数通常按我们所知的方式使用。八进制数以 0 字符开头,后跟八进制数。十六进制数以 0x 字符开头,后跟十六进制数。二进制数以 0b 开头,后跟二进制数(零和一)。

Main.java
void main() {

    int n1 = 31;
    int n2 = 0x31;
    int n3 = 031;
    int n4 = 0b1001;

    System.out.println(n1);
    System.out.println(n2);
    System.out.println(n3);
    System.out.println(n4);
}

我们有四个整数变量。每个变量都使用不同的整数表示法分配了一个值。

int n1 = 31;
int n2 = 0x31;
int n3 = 031;
int n4 = 0b1001;

第一个是十进制,第二个是十六进制,第三个是八进制,第四个是二进制。

$ java Main.java
31
49
25
9

大数字很难阅读。如果我们有一个像 245342395423452 这样的数字,我们发现很难快速读取它。在计算机之外,大数字用空格或逗号分隔。自从 Java SE 1.7 以来,可以使用下划线分隔整数。

下划线不能用在数字的开头或结尾,紧邻浮点文字中的小数点,以及在 FL 后缀之前。

Main.java
void main() {

    long a = 23482345629L;
    long b = 23_482_345_629L;

    System.out.println(a == b);
}

此代码示例演示了 Java 中下划线的使用。

long a = 23482345629L;
long b = 23_482_345_629L;

我们有两个相同的长数字。在第二个数字中,我们每三个数字分隔一次。比较这两个数字,我们得到一个布尔值 true。L 后缀告诉编译器我们有一个长数字文字。

Java byteshortintlong 类型用于表示固定精度数字。这意味着它们可以表示有限数量的整数。long 类型可以表示的最大整数是 9223372036854775807。如果我们要处理更大的数字,则必须使用 java.math.BigInteger 类。它用于表示不可变的任意精度整数。任意精度整数仅受可用计算机内存量的限制。

Main.java
import java.math.BigInteger;

void main() {

    System.out.println(Long.MAX_VALUE);

    BigInteger b = new BigInteger("92233720368547758071");
    BigInteger c = new BigInteger("52498235605326345645");

    BigInteger a = b.multiply(c);

    System.out.println(a);
}

java.math.BigInteger 类的帮助下,我们将两个非常大的数字相乘。

System.out.println(Long.MAX_VALUE);

我们打印可以由 long 类型表示的最大整数值。

BigInteger b = new BigInteger("92233720368547758071");
BigInteger c = new BigInteger("52498235605326345645");

我们定义两个 BigInteger 对象。它们都保存比 long 类型可以保存的更大的值。

BigInteger a = b.multiply(c);

使用 multiply 方法,我们将两个数字相乘。请注意,BigInteger 数字是不可变的。该操作返回一个新值,我们将其分配给一个新变量。

System.out.println(a);

计算出的整数被打印到控制台。

$ java Main.java
9223372036854775807
4842107582663807707870321673775984450795

算术溢出

算术溢出是一种当计算产生的结果在幅度上大于给定的寄存器或存储位置可以存储或表示的结果时发生的条件。

Main.java
void main() {

    byte a = 126;

    System.out.println(a);
    a++;

    System.out.println(a);
    a++;

    System.out.println(a);
    a++;

    System.out.println(a);
}

在此示例中,我们尝试分配一个超出数据类型范围的值。这会导致算术溢出。

$ java Main.java
126
127
-128
-127

发生溢出时,变量将重置为负上限值。

浮点数

实数测量连续量,如重量、高度或速度。浮点数表示计算机中实数的近似值。在 Java 中,我们有两种原始浮点类型:floatdoublefloat 是一种单精度类型,它以 32 位存储数字。

double 是一种双精度类型,它以 64 位存储数字。这两种类型都具有固定精度,并且不能完全表示所有实数。在必须使用精确数字的情况下,我们可以使用 BigDecimal 类。

带有 F/f 后缀的浮点数是 float 类型,double 数字带有 D/d 后缀。double 数字的后缀是可选的。

假设一位 100 米短跑运动员跑了 9.87 秒。他的速度是多少公里/小时?

Main.java
void main() {

    float distance;
    float time;
    float speed;

    distance = 0.1f;

    time = 9.87f / 3600;

    speed = distance / time;

    System.out.format("The average speed of a sprinter is %f km/h%n", speed);
}

在此示例中,必须使用浮点值。在这种情况下,float 数据类型的低精度不会造成问题。

distance = 0.1f;

100 米是 0.1 公里。

time = 9.87f / 3600;

9.87 秒是 9.87/60*60 小时。

speed = distance / time;

为了获得速度,我们将距离除以时间。

$ java Main.java
The average speed of a sprinter is 36.474163 km/h

这是程序的输出。数字中的一个小舍入误差不会影响我们对短跑运动员速度的理解。

floatdouble 类型是不精确的。

Main.java
void main() {

    double a = 0.1 + 0.1 + 0.1;
    double b = 0.3;

    System.out.println(a);
    System.out.println(b);

    System.out.println(a == b);
}

该代码示例说明了浮点值的不精确性质。

double a = 0.1 + 0.1 + 0.1;
double b = 0.3;

我们定义两个 double 值。D/d 后缀是可选的。乍一看,它们应该是相等的。

System.out.println(a);
System.out.println(b);

打印它们会显示一个非常小的差异。

System.out.println(a == b);

此行将返回 false。

$ java Main.java
0.30000000000000004
0.3
false

存在一个很小的边际误差。因此,比较运算符返回一个布尔值 false。

当我们处理金钱、货币以及通常在商业应用程序中时,我们需要使用精确的数字。基本浮点类型的舍入误差是不可接受的。

Main.java
void main() {

    float c = 1.46f;
    float sum = 0f;

    for (int i=0; i<100_000; i++) {

        sum += c;
    }

    System.out.println(sum);
}

1.46f 表示 1 欧元和 46 美分。我们从 100000 个这样的金额中创建一个总和。

for (int i=0; i<100_000; i++) {

    sum += c;
}

在此循环中,我们从 100000 个这样的金额中创建一个总和。

$ java Main.java
146002.55

计算会导致 2 欧元和 55 美分的误差。

为了避免此边际误差,我们利用 BigDecimal 类。它用于保存不可变的、任意精度的有符号十进制数。

Main.java
import java.math.BigDecimal;

void main() {

    BigDecimal c = new BigDecimal("1.46");
    BigDecimal sum = new BigDecimal("0");

    for (int i=0; i<100_000; i++) {

        sum = sum.add(c);
    }

    System.out.println(sum);
}

我们用相同的金额执行相同的操作。

BigDecimal c = new BigDecimal("1.46");
BigDecimal sum = new BigDecimal("0");

我们定义两个 BigDecimal 数字。

for (int i=0; i<100_000; i++) {

    sum = sum.add(c);
}

BigDecimal 数字是不可变的,因此在每个循环中始终将一个新对象分配给 sum 变量。

$ java Main.java
146000.00

在此示例中,我们获得了精确的值。

Java 支持浮点值的科学语法。也称为指数表示法,它是一种写入太大或太小而无法方便地以标准十进制表示法写入的数字的方法。

Main.java
import java.math.BigDecimal;
import java.text.DecimalFormat;

void main() {

    double n = 1.235E10;
    DecimalFormat dec = new DecimalFormat("#.00");

    System.out.println(dec.format(n));

    BigDecimal bd = new BigDecimal("1.212e-19");

    System.out.println(bd.toEngineeringString());
    System.out.println(bd.toPlainString());
}

我们使用科学计数法定义两个浮点值。

double n = 1.235E10;

这是 double 类型的浮点值,以科学计数法写入。

DecimalFormat dec = new DecimalFormat("#.00");

System.out.println(dec.format(n));

我们使用 DecimalFormat 类将我们的 double 值排列成标准十进制格式。

BigDecimal bd = new BigDecimal("1.212e-19");

System.out.println(bd.toEngineeringString());
System.out.println(bd.toPlainString());

BigDecimal 类采用科学计数法中的浮点值作为参数。我们使用该类的两个方法来打印工程和普通字符串中的值。

$ java Main.java
12350000000.00
121.2E-21
0.0000000000000000001212

枚举

枚举类型是一种特殊的数据类型,它使变量可以成为一组预定义的常量。已声明为具有枚举类型的变量可以被分配任何枚举器作为值。枚举使代码更具可读性。当我们处理只能从一小组可能值中取一个值的变量时,枚举很有用。

Main.java
enum Days {

    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY
}

void main() {

    Days day = Days.MONDAY;

    if (day == Days.MONDAY) {

        System.out.println("It is Monday");
    }

    System.out.println(day);

    for (Days d : Days.values()) {

        System.out.println(d);
    }
}

在我们的代码示例中,我们为星期几创建一个枚举。

enum Days {

    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY
}

表示一周中几天的枚举是用 enum 关键字创建的。枚举的项是常量。按照惯例,常量以大写字母书写。

Days day = Days.MONDAY;

我们有一个名为 day 的变量,它是 Days 的枚举类型。它被初始化为 Monday。

if (day == Days.MONDAY) {

    System.out.println("It is Monday");
}

此代码比将 day 变量与某个数字进行比较更具可读性。

System.out.println(day);

此行将 Monday 打印到控制台。

for (Days d : Days.values()) {

    System.out.println(d);
}

此循环将所有天打印到控制台。values 方法返回一个数组,其中包含此 enum 类型的常量,按照它们声明的顺序。此方法可用于使用增强的 for 语句迭代常量。增强的 for 遍历数组,逐个元素,并将它们打印到终端。

$ java Main.java
It is Monday
MONDAY
MONDAY
TUESDAY
WEDNESDAY
THURSDAY
FRIDAY
SATURDAY
SUNDAY

可以为枚举常量赋予一些值。

Main.java
enum Season {

    SPRING(10),
    SUMMER(20),
    AUTUMN(30),
    WINTER(40);

    private int value;

    Season(int value) {
        this.value = value;
    }

    public int getValue() {

        return value;
    }
}

void main() {

    for (Season season : Season.values()) {
        System.out.println(season + " " + season.getValue());
    }
}

该示例包含一个具有四个常量的 Season 枚举。

SPRING(10),
SUMMER(20),
AUTUMN(30),
WINTER(40);

在这里,我们定义了 enum 的四个常量。这些常量被赋予特定的值。

private int value;

private Season(int value) {
    this.value = value;
}

当我们定义常量时,我们还必须创建一个构造函数。构造函数将在本教程后面介绍。

$ java Main.java
SPRING 10
SUMMER 20
AUTUMN 30
WINTER 40

字符串和字符

String 是一种表示计算机程序中文本数据的数据类型。Java 中的字符串是一个字符序列。char 是单个字符。字符串用双引号括起来。

Main.java
void main() {

    String word = "ZetCode";

    char c = word.charAt(0);
    char d = word.charAt(3);

    System.out.println(c);
    System.out.println(d);
}

该程序将 Z 字符打印到终端。

String word = "ZetCode";

在这里,我们创建一个字符串变量并将其赋值为“ZetCode”值。

char c = word.charAt(0);

charAt 方法返回指定索引处的 char 值。序列的第一个字符值位于索引 0 处,下一个字符值位于索引 1 处,依此类推。

$ java Main.java
Z
C

该程序将“ZetCode”字符串的第一个和第四个字符打印到控制台。

数组

数组是一种复杂的数据类型,它处理元素的集合。可以通过索引访问每个元素。数组的所有元素必须具有相同的数据类型。

Main.java
void main() {

    int[] numbers = new int[5];

    numbers[0] = 3;
    numbers[1] = 2;
    numbers[2] = 1;
    numbers[3] = 5;
    numbers[4] = 6;

    int len = numbers.length;

    for (int i = 0; i < len; i++) {

        System.out.println(numbers[i]);
    }
}

在此示例中,我们声明一个数组,用数据填充它,然后将数组的内容打印到控制台。

int[] numbers = new int[5];

我们创建一个最多可以存储 5 个整数的整数数组。因此,我们有一个包含五个元素的数组,索引为 0..4。

numbers[0] = 3;
numbers[1] = 2;
numbers[2] = 1;
numbers[3] = 5;
numbers[4] = 6;

在这里,我们将值分配给创建的数组。我们可以通过数组访问表示法访问数组的元素。它由数组名称后跟方括号组成。在方括号内,我们指定要访问的元素的索引。

int len = numbers.length;

每个数组都有一个 length 属性,该属性返回数组中元素的数量。

for (int i = 0; i < len; i++) {

    System.out.println(numbers[i]);
}

我们遍历数组并将数据打印到控制台。

$ java Main.java
3
2
1
5
6

Java 包装类

包装类是原始数据类型的对象表示形式。当需要 Object 时,包装类用于表示原始值。例如,Java 集合仅适用于对象。它们不能接受原始类型。包装类还包括一些有用的方法。例如,它们包括用于执行数据类型转换的方法。将原始类型放入包装类称为 装箱。反向过程称为 拆箱

作为一般规则,当有充分的理由时,我们会使用包装类。否则,我们会使用原始类型。包装类是不可变的。一旦创建它们,就无法更改它们。原始类型比装箱类型更快。在科学计算和其他大规模数字处理中,包装类可能会导致显着的性能下降。

原始类型 包装类 构造函数参数
byte Byte byte 或 String
short Short short 或 String
int Integer int 或 String
long Long long 或 String
float Float float、double 或 String
double Double double 或 String
char 字符 char
布尔值 Boolean boolean 或 String
表:原始类型及其等效的包装类

Integer 类将原始类型 int 的值包装在一个对象中。它包含处理 int 时有用的常量和方法。

Main.java
void main() {

    int a = 55;
    Integer b = Integer.valueOf(a);

    int c = b.intValue();
    float d = b.floatValue();

    String bin = Integer.toBinaryString(a);
    String hex = Integer.toHexString(a);
    String oct = Integer.toOctalString(a);

    System.out.println(a);
    System.out.println(b);
    System.out.println(c);
    System.out.println(d);

    System.out.println(bin);
    System.out.println(hex);
    System.out.println(oct);
}

此示例使用 Integer 包装类。

int a = 55;

此行创建一个整数原始数据类型。

Integer b = Integer.valueOf(a);

从原始 int 类型创建一个 Integer 包装类。

int c = b.intValue();
float d = b.floatValue();

intValue 方法将 Integer 转换为 int。同样,floatValue 返回一个 float 数据类型。

String bin = Integer.toBinaryString(a);
String hex = Integer.toHexString(a);
String oct = Integer.toOctalString(a);

这三种方法返回整数的二进制、十六进制和八进制表示形式。

$ java Main.java
55
55
55
55.0
110111
37
67

集合是用于处理对象组的强大工具。原始数据类型不能放入 Java 集合中。在装箱原始值之后,我们可以将它们放入集合中。

Main.java
import java.util.List;

void main() {

    List<Number> ls = new ArrayList<>();

    ls.add(1342341);
    ls.add(Float.valueOf(34.56f));
    ls.add(235.242);
    ls.add(Byte.valueOf("102"));
    ls.add(Short.valueOf("1245"));

    for (Number n : ls) {

        System.out.println(n.getClass());
        System.out.println(n);
    }
}

在该示例中,我们将各种数字放入 ArrayList 中。ArrayList 是一个动态的、可调整大小的数组。

List<Number> ls = new ArrayList<>();

创建了一个 ArrayList 实例。在尖括号中,我们指定容器将保存的类型。 Number 是 Java 中所有五个数字原始类型的抽象基类。

ls.add(1342341);
ls.add(Float.valueOf(34.56f));
ls.add(235.242);
ls.add(Byte.valueOf("102"));
ls.add(Short.valueOf("1245"));

我们将五个数字添加到集合中。请注意,整数和双精度值没有被装箱;这是因为对于整数和双精度类型,编译器会执行自动装箱。

for (Number n : ls) {

    System.out.println(n.getClass());
    System.out.println(n);
}

我们遍历容器并打印每个元素的类名及其值。

$ java Main.java
class java.lang.Integer
1342341
class java.lang.Float
34.56
class java.lang.Double
235.242
class java.lang.Byte
102
class java.lang.Short
1245

com.zetcode.Numbers 程序给出此输出。请注意,这两个数字已由编译器自动装箱。

Java 装箱

从原始类型转换为对象类型称为 装箱拆箱 是相反的操作。它是将对象类型转换回原始类型。

Main.java
void main() {

    long a = 124235L;

    Long b = Long.valueOf(a);
    long c = b.longValue();

    System.out.println(c);
}

在代码示例中,我们将一个 long 值装箱到 Long 对象中,反之亦然。

Long b = Long.valueOf(a);

此行执行装箱。

long c = b.longValue();

在此行中,我们进行拆箱。

Java 自动装箱

Java 5 引入了自动装箱。 自动装箱 是原始类型及其相应的对象包装类之间的自动转换。自动装箱使编程更容易。程序员不需要手动进行转换。

当一个值是原始类型,另一个值是包装类时,会执行自动装箱和拆箱:

Integer i = new Integer(50);

if (i < 100) {
   ...
}

在 if 表达式的方括号内,将一个 Integer 与一个 int 进行比较。 Integer 对象被转换为原始 int 类型并与 100 值进行比较。 进行了自动拆箱。

Main.java
int cube(int x) {

    return x * x * x;
}

void main() {

    Integer i = 10;
    int j = i;

    System.out.println(i);
    System.out.println(j);

    Integer a = cube(i);
    System.out.println(a);
}

在此代码示例中演示了自动装箱和自动拆箱。

Integer i = 10;

Java 编译器在此代码行中执行自动装箱。 一个 int 值被装箱到 Integer 类型。

int j = i;

这里发生了自动拆箱。

Integer a = cube(i);

当我们把一个 Integer 传递给 cube 方法时,会自动拆箱。 当我们返回计算出的值时,会自动装箱,因为一个 int 会被转换回 Integer

Java 语言不支持运算符重载。 当我们对包装类应用算术运算时,编译器会自动装箱。

Main.java
void main() {

    Integer a = Integer.valueOf(5);
    Integer b = Integer.valueOf(7);

    Integer add = a + b;
    Integer mul = a * b;

    System.out.println(add);
    System.out.println(mul);
}

我们有两个 Integer 值。 我们对这两个值执行加法和乘法运算。

Integer add = a + b;
Integer mul = a * b;

与 Ruby、C#、Python、D 或 C++ 等语言不同,Java 没有实现运算符重载。 在这两行中,编译器调用 intValue 方法并将包装类转换为 int,然后通过调用 valueOf 方法将结果包装回 Integer

Java 自动装箱和对象驻留

对象驻留 是仅存储每个不同对象的一个副本。 该对象必须是不可变的。 不同的对象存储在驻留池中。 在 Java 中,当原始值被装箱到包装器对象中时,某些值(任何布尔值、任何字节、任何从 0 到 127 的 char 以及 -128 和 127 之间的任何 shortint)都会被驻留,并且保证对这些值中的一个值进行的任何两次装箱转换都会产生相同的对象。

根据 Java 语言规范,这些是最小范围。 因此,该行为取决于实现。 对象驻留可以节省时间和空间。 从文字、自动装箱和 Integer.valueOf 获得的对象是驻留对象,而使用 new 运算符构造的对象始终是不同的对象。

在比较包装类时,对象驻留有一些重要的后果。 == 运算符比较对象的引用标识,而 equals 方法比较值。

Main.java
void main() {

    Integer a = 5; // Integer.valueOf(5);
    Integer b = 5; // Integer.valueOf(5);

    System.out.println(a == b);
    System.out.println(a.equals(b));
    System.out.println(a.compareTo(b));

    Integer c = 155;
    Integer d = 155;

    System.out.println(c == d);
    System.out.println(c.equals(d));
    System.out.println(c.compareTo(d));
}

该示例比较了一些 Integer 对象。

Integer a = 5; // Integer.valueOf(5);
Integer b = 5; // Integer.valueOf(5);

两个整数被装箱到 Integer 包装类中。

System.out.println(a == b);
System.out.println(a.equals(b));
System.out.println(a.compareTo(b));

使用三种不同的方式来比较这些值。 == 运算符比较两个装箱类型的引用标识。 由于对象驻留,操作结果为 true。

如果我们使用 new 运算符,将会创建两个不同的对象,并且 == 运算符将返回 false。 equals 方法以数值方式比较两个 Integer 对象。 它返回布尔值 true 或 false(在我们的例子中为 true)。

最后,compareTo 方法也以数值方式比较两个对象。 如果此 Integer 等于参数 Integer,则返回 0 值; 如果此 Integer 在数值上小于参数 Integer,则返回小于 0 的值; 如果此 Integer 在数值上大于参数 Integer,则返回大于 0 的值。

Integer c = 155;
Integer d = 155;

我们还有另外两种装箱类型。 但是,这些值大于驻留的最大值 (127); 因此,创建了两个不同的对象。 这次 == 运算符产生 false。

$ java Main.java
true
true
0
false
true
0

Java 空类型

Java 有一个特殊的 null 类型。 该类型没有名称。 因此,不可能声明 null 类型的变量或强制转换为 null 类型。 null 表示空引用,不引用任何对象。 null 是引用类型变量的默认值。 原始类型不能分配 null 文字。

在不同的上下文中,null 表示缺少对象、未知值或未初始化的状态。

Main.java
import java.util.Random;

String getName() {

    Random r = new Random();
    boolean n = r.nextBoolean();

    if (n == true) {

        return "John";
    } else {

        return null;
    }
}

void main() {

    String name = getName();
    System.out.println(name);
    System.out.println(null == null);

    if ("John".equals(name)) {

        System.out.println("His name is John");
    }
}

我们在程序中使用 null 值。

String getName() {

    Random r = new Random();
    boolean n = r.nextBoolean();

    if (n == true) {

        return "John";
    } else {

        return null;
    }
}

getName 方法中,我们模拟一种方法有时会返回 null 值的情况。

System.out.println(null == null);

我们比较两个空值。 该表达式返回 true。

if ("John".equals(name)) {

    System.out.println("His name is John");
}

我们将 name 变量与 "John" 字符串进行比较。 请注意,我们在 "John" 字符串上调用 equals 方法。 这是因为如果 name 变量等于 null,则调用该方法会导致 NullPointerException

$ java Main.java
null
true
$ java Main.java
null
true
$ java Main.java
John
true
His name is John

Java 默认值

编译器为未初始化的字段赋予默认值。 最终字段和局部变量必须由开发人员初始化。

下表显示了不同类型的默认值。

数据类型 默认值
byte 0
char '\u0000'
short 0
int 0
long 0L
float 0f
double 0d
对象 null
布尔值 false
表:未初始化的实例变量的默认值

下一个示例将打印未初始化的实例变量的默认值。 实例变量是在类中定义的变量,该类的每个实例化对象都有一个单独的副本。

Main.java
byte b;
char c;
short s;
int i;
float f;
double d;
String str;
Object o;

void main() {

    System.out.println(b);
    System.out.println(c);
    System.out.println(s);
    System.out.println(i);
    System.out.println(f);
    System.out.println(d);
    System.out.println(str);
    System.out.println(o);
}

在该示例中,我们声明了八个成员字段。 它们未初始化。 编译器将为每个字段设置一个默认值。

byte b;
char c;
short s;
int i;
...

这些是实例变量; 它们在任何方法之外声明。

$ java Main.java
0

0
0
0.0
0.0
null
null

Java 类型转换

我们经常一次处理多种数据类型。 将一种数据类型转换为另一种数据类型是编程中的一项常见工作。 术语类型转换是指将一种数据类型的实体更改为另一种数据类型。 在本节中,我们将处理原始数据类型的转换。 引用类型转换将在本章稍后提到。 转换规则很复杂; 它们在 Java 语言规范的第 5 章中指定。

有两种类型的转换:隐式转换和显式转换。 隐式类型转换,也称为强制转换,是编译器自动进行的类型转换。 在显式转换中,程序员直接在一对圆括号内指定转换类型。 显式转换称为类型转换

转换发生在不同的上下文中:赋值、表达式或方法调用。

int x = 456;
long y = 34523L;
float z = 3.455f;
double w = 6354.3425d;

在这四个赋值中,没有发生转换。 每个变量都被分配一个预期类型的文字。

int x = 345;
long y = x;

float m = 22.3354f;
double n = m;

在此代码中,Java 编译器隐式地执行了两个转换。 将较小类型的变量分配给较大类型的变量是合法的。 该转换被认为是安全的,因为没有精度损失。 这种类型的转换称为隐式扩大转换

long x = 345;
int y = (int) x;

double m = 22.3354d;
float n = (float) m;

在 Java 中,将较大类型的变量分配给较小类型是不合法的。 即使值本身适合较小类型的范围。 在这种情况下,可能会丢失精度。 为了允许这样的赋值,我们必须使用类型转换操作。 这样,程序员就表示他是有意为之,并且他意识到可能会有一些精度损失。 这种类型的转换称为显式缩小转换

byte a = 123;
short b = 23532;

在这种情况下,我们处理一种特定的赋值转换。 123 和 23532 是整数文字,a 和 b 变量是 byteshort 类型。 可以使用强制转换操作,但不是必需的。 这些文字可以在赋值左侧的变量中表示。 我们处理的是隐式缩小转换

byte calc(byte x) {
...
}
byte b = calc((byte) 5);

上述规则仅适用于赋值。 当我们将整数文字传递给需要字节的方法时,我们必须执行强制转换操作。

Java 数值提升

数值提升是一种特定的隐式类型转换。 它发生在算术表达式中。 数值提升用于将数值运算符的操作数转换为公共类型,以便可以执行操作。

int x = 3;
double y = 2.5;
double z = x + y;

在第三行中,我们有一个加法表达式。 x 操作数是 int,y 操作数是 double。 编译器将整数转换为双精度值并将两个数字相加。 结果是一个双精度值。 这是一个隐式扩大原始类型转换的例子。

byte a = 120;
a = a + 1; // compilation error

此代码会导致编译时错误。 在第二行的右侧,我们有一个字节变量 a 和一个整数文字 1。 该变量被转换为整数,并且将这些值相加。 结果是一个整数。 稍后,编译器尝试将该值分配给 a 变量。 在没有显式强制转换运算符的情况下,将较大类型分配给较小类型是不可能的。 因此,我们收到一个编译时错误。

byte a = 120;
a = (byte) (a + 1);

此代码确实可以编译。 请注意用于 a + 1 表达式的圆括号。 (byte) 强制转换运算符的优先级高于加法运算符。 如果我们想将强制转换应用于整个表达式,我们必须使用圆括号。

byte a = 120;
a += 5;

复合运算符自动执行隐式转换。

short r = 21;
short s = (short) -r;

在变量上应用 +- 一元运算符时,会执行一元数值提升。 short 类型被提升为 int 类型。 因此,我们必须使用强制转换运算符才能通过赋值。

byte u = 100;
byte v = u++;

在一元递增 ++、递减 -- 运算符的情况下,不进行转换。 强制转换不是必需的。

Java 装箱、拆箱转换

装箱转换将原始类型的表达式转换为相应的包装器类型的表达式。 拆箱转换将包装器类型的表达式转换为相应的原始类型的表达式。 从 booleanBoolean 或从 byte 到 Byte 的转换是装箱转换的示例。 相反的转换,例如从 Booleanboolean 或从 Bytebyte 是拆箱转换的示例。

Byte b = 124;
byte c = b;

在第一行代码中,Java 编译器执行自动装箱转换。 在第二行中,完成拆箱转换。

String checkAge(Short age) {
...
}
String r = checkAge((short) 5);

这里我们有一个方法调用上下文中的装箱转换。 我们将一个 short 类型传递给需要 Short 包装器类型的方法。 该值被装箱。

Boolean gameOver = new Boolean("true");

if (gameOver) {
    System.out.println("The game is over");
}

这是一个拆箱转换的示例。 在 if 表达式内部,调用 booleanValue 方法。 该方法将 Boolean 对象的值作为 boolean 原始类型返回。

对象引用转换

对象、接口和数组是引用数据类型。 任何引用都可以强制转换为 Object。 对象类型确定在运行时使用哪种方法。 引用类型确定在编译时将使用哪种重载方法。

接口类型只能转换为接口类型或 Object。 如果新类型是一个接口,则它必须是旧类型的父接口。 类类型可以转换为类类型或接口类型。 如果转换为类类型,则新类型必须是旧类型的超类。 如果转换为接口类型,则旧类必须实现该接口。 数组可以转换为类 Object、接口 CloneableSerializable,或者转换为数组。

有两种类型的引用变量强制转换:向下转换和向上转换。 向上转换(泛化或扩大)是从子类型到父类型的强制转换。 我们正在将单个类型强制转换为公共类型。 向下转换(专门化或缩小)是从父类型到子类型的强制转换。 我们正在将公共类型强制转换为单个类型。

向上转换缩小了可用于对象的方法和属性的列表,而向下转换可以扩展它。 向上转换是安全的,但向下转换涉及类型检查,并且可能会抛出 ClassCastException

Main.java
import java.util.Random;

class Animal {}
class Mammal extends Animal {}
class Dog extends Animal {}
class Cat extends Animal {}

void main() {

    // upcasting
    Animal animal = new Dog();
    System.out.println(animal);

    // ClassCastException
    // Mammal mammal = (Mammal) new Animal();

    var returned = getRandomAnimal();

    if (returned instanceof Cat cat) {

        System.out.println(cat);
    } else if (returned instanceof Dog dog) {

        System.out.println(dog);
    } else if (returned instanceof Mammal mammal) {

        System.out.println(mammal);
    } else {

        System.out.println(returned);
    }
}

Animal getRandomAnimal() {

    int val = new Random().nextInt(4) + 1;

    Animal anim = switch (val) {

        case 2 -> new Mammal();
        case 3 -> new Dog();
        case 4 -> new Cat();
        default -> new Animal();
    };

    return anim;
}

该示例执行引用类型转换。

// upcasting
Animal animal = new Dog();
System.out.println(animal);

我们将从子类型 Dog 强制转换为父类型 Animal。 这是向上转换,并且始终是安全的。

// ClassCastException
// Mammal mammal = (Mammal) new Animal();

Animal 向下转换为 Mammal 会导致 ClassCastException

var returned = getRandomAnimal();

if (returned instanceof Cat cat) {

    System.out.println(cat);
} else if (returned instanceof Dog dog) {

    System.out.println(dog);
} else if (returned instanceof Mammal mammal) {

    System.out.println(mammal);
} else {

    System.out.println(returned);
}

为了执行合法的向下转换,我们需要首先使用 instanceof 运算符检查对象的类型。

Animal getRandomAnimal() {

    int val = new Random().nextInt(4) + 1;

    Animal anim = switch (val) {

        case 2 -> new Mammal();
        case 3 -> new Dog();
        case 4 -> new Cat();
        default -> new Animal();
    };

    return anim;
}

getRandomAnimal 使用 Java 的 swith 表达式返回一个随机动物。

Java 字符串转换

在编程中,在数字和字符串之间执行字符串转换非常常见。 不允许强制转换操作,因为字符串和原始类型是根本不同的类型。 有几种方法可以进行字符串转换。 对于 + 运算符,还有一种自动字符串转换。

有关字符串转换的更多信息将在本教程的字符串章节中介绍。

String s = (String) 15; // compilation error
int i = (int) "25"; // compilation error

不可能在数字和字符串之间进行强制转换。 相反,我们有各种方法可以在数字和字符串之间进行转换。

short age = Short.parseShort("35");
int salary = Integer.parseInt("2400");
float height = Float.parseFloat("172.34");
double weight = Double.parseDouble("55.6");

包装器类的 parse 方法将字符串转换为原始类型。

Short age = Short.valueOf("35");
Integer salary = Integer.valueOf("2400");
Float height = Float.valueOf("172.34");
Double weight = Double.valueOf("55.6");

valueOf 方法从原始类型返回包装器类。

int age = 17;
double weight = 55.3;
String v1 = String.valueOf(age);
String v2 = String.valueOf(weight);

String 类有一个 valueOf 方法,用于将各种类型转换为字符串。

当使用 + 运算符并且一个运算符是字符串,而另一个运算符不是字符串时,会发生自动字符串转换。 非字符串操作数 + 被转换为字符串。

Main.java
void main() {

    String name = "Jane";
    short age = 17;

    System.out.println(name + " is " +  age + " years old.\n");
}

在这个例子中,我们有一个 String 数据类型和一个 short 数据类型。这两种类型使用 + 运算符连接成一个句子。

System.out.println(name + " is " +  age + " years old.");

在这个表达式中,age 变量被转换为 String 类型。

$ java Main.java
Jane is 17 years old.

来源

Java 语言基础 - 教程

在本文中,我们介绍了 Java 中的数据类型。

作者

我的名字是 Jan Bodnar,我是一位充满热情的程序员,拥有丰富的编程经验。 自 2007 年以来,我一直在撰写编程文章。 迄今为止,我已经撰写了超过 1,400 篇文章和 8 本电子书。 我拥有超过十年的编程教学经验。

列出所有Java教程