第二章 变量

252 阅读38分钟

现在我们已经熟悉了NetBeans并且对Java程序有了一些基本的了解,让我们开始真正的学习。在本章中,我们将学习有关变量的所有知识。我们将学习什么是变量以及如何命名、声明和初始化它们。同时,我们将学习可以对它们执行的常见操作。

2.1 变量

变量是我们需要在程序中存储和操作的数据的名称。例如,假设您的程序需要存储用户的年龄。为此,我们可以将此数据命名为userAge,并使用以下语句声明变量userAge

int userAge;

此声明语句首先声明变量的数据类型,然后是其名称。变量的数据类型是指变量将存储的数据类型(例如,它是数字还是文本)。Java是一种强类型语言。首先,每个变量都有一个类型,每个表达式都有一个类型,并且每个类型都是严格定义的。其次,所有赋值,无论是显式的还是通过方法调用中的参数传递,都会检查类型的兼容性。与某些语言中的自动强制转换或冲突类型的转换不同,Java编译器检查所有表达式和参数以确保类型兼容。任何类型不匹配的错误都是必须在编译器完成编译之前纠正的。在我们的示例中,数据类型是int,表示整数。我们的变量名称是userAge。 声明变量userAge后,程序将分配一定的计算机内存空间来存储此数据。然后,您可以通过引用它的名称userAge来访问和修改此数据。

2.2 原始数据类型

有八种在Java中预定义的基本数据类型,它们被称为原始数据类型。其中前四种数据类型用于存储整数(即没有小数部分的数字),它们分别是:byte、short、int和long。所有这些类型都是有符号的,包括正数和负数。Java不支持无符号的、仅为正的整数。Java的设计者认为无符号整数是不必要的。在第3章中,我们将会学到Java通过添加特殊的“无符号右移”运算符来不同地管理高位比特的含义。

类型名称关键字占用内存取值范围
字节型byte1 字节-128~127
短整型short2 字节-32768~32767
整型int4 字节-2147483648~2147483647
长整型long8 字节-9223372036854775808L~9223372036854775807L
单精度浮点型float4 字节+/-3.4E+38F(6~7 个有效位)
双精度浮点型double8 字节+/-1.8E+308 (15 个有效位)
字符型char2 字节ISO 单一字符集
布尔型boolean1 字节true 或 false

整数类型的宽度不应被视为它消耗的存储量,而应被视为它为该类型的变量和表达式定义的行为。只要类型的行为与声明的行为相同,Java运行时环境就可以自由地使用任何大小。

byte

在Java中,byte是一种原始数据类型,用于表示8位的有符号整数。它使用1个字节的存储空间(这被称为数据类型的宽度或大小)。它的取值范围为-128到127。由于byte类型所占用的内存空间很小,因此在对内存空间要求严格的场合下,可以使用byte类型来节省内存。例如,在处理音频或图像数据时,可以使用byte类型来存储像素或采样数据。

short

在Java中,short是一种原始数据类型,用于表示16位的有符号整数。它的取值范围为-32,768到32,767。虽然short类型比byte类型占用更多的内存空间,但它可以存储更大的整数,因此可以在一些需要处理较大整数的场合下使用。

int

在Java中,int是一种原始数据类型,用于表示32位的有符号整数。它的取值范围为-2,147,483,648到2,147,483,647。int通常是最常用的整数类型,因为它可以存储大多数常见的整数值,并且在大多数情况下具有良好的性能。例如,可以使用int类型来表示计数器、索引、ID等整数值。

long

长整型(long )使用8字节的存储空间,范围为-2^{63}到2^{63}-1。它很少被使用,除非你真的需要存储一个非常大的整数(比如地球上的居民数量)。为了指定一个长整型的值,你需要在数字末尾添加后缀“L”。关于后缀,我们将在下一节详细讨论。

除了用于存储整数的数据类型外,我们还有用于存储浮点数(即带有小数部分的数字)的数据类型。它们包括:

float

float代表单精度浮点数据类型,它使用4个字节(32位)的存储空间,其范围约为负3.40282347 \times 10^{38} 到正3.40282347 \times 10^{38} 。它的精度约为7个数字。这意味着如果您使用浮点类型来存储一个像1.23456789(10个数字)这样的数字,该数字将会被四舍五入到约7个数字(即1.234568)。

float hightemp, lowtemp;

double

double代表双精度浮点型,双精度数据类型使用8个字节的存储空间,其范围约为负1.79769313486231570 \times 10^{308} 到正1.79769313486231570 \times 10^{308} ,精度约为15个数字。默认情况下,在Java中指定浮点数时,它会自动被视为double,而不是float。如果您想让Java将浮点数视为float,必须在数字末尾添加后缀“F”。除非内存空间是一个问题,否则您应该始终使用double而不是float,因为它更精确。在某些为高速数学计算优化的现代处理器上,双精度实际上比单精度更快。所有超越数学函数,如sin(),cos()和sqrt(),都返回double值。当您需要在许多迭代计算中保持精度,或者正在操作大量数字时,double是最好的选择。

下面一段程序是计算圆的面积的样例:

//计算圆的面积
public class CircleArea {
​
    public static void main(String[] args) {
        double pi,r,a;
        r=10.8;//圆的半径
        pi=3.1416;//pi的近似
        a=pi*r*r;//面积计算公式
        
        System.out.println("Area of the circle is"+a);
    }
}

除了上述六种数据类型之外,Java还有两种原始数据类型。它们是:

char

在Java中,用于存储字符的数据类型是char。Java使用Unicode表示字符。Unicode定义了一个完全国际化的字符集,可以表示所有人类语言中的所有字符。它是数十种字符集的统一,例如拉丁、希腊、阿拉伯、西里尔、希伯来、片假名、韩文等等。在Java创建时,Unicode需要16位。因此,在Java中,char是一个16位类型。char的范围是0到65,536。没有负数的char。标准字符集ASCII仍然从0到127,扩展的8位字符集ISO-Latin-1的范围从0到255。由于Java的设计允许编写面向全球使用的程序,因此使用Unicode表示字符是有意义的。当然,对于像英语、德语、西班牙语或法语这样的语言来说,使用Unicode有些低效,因为这些语言的字符可以轻松地包含在8位内。但是这是必须为全球可移植性支付的代价。

下面用一个程序展示字符型类型:

//展示字符数据类型的示例程序
public class CharDemo {
    public static void main(String[] args) {
        char ch1,ch2;
        
        ch1=88;//X在unicode中的表示
        ch2='Y';
        
        System.out.print("ch1 and ch2: ");
        System.out.println(ch1+" "+ch2);
        
    }
}

这个程序的输出为:

ch1 and ch2: X Y

ch1被赋值为88,这是对应于字母X的ASCII(和Unicode)值。ASCII字符集占据了Unicode字符集的前127个值。因此,您在其他语言中使用的所有“老技巧”在Java中也同样适用。

虽然char被设计用于存储Unicode字符,但它也可以作为整数类型使用,您可以在其上执行算术运算。例如,您可以将两个字符相加,或者增加字符变量的值。如下所示:

package com.mycompany.chardemo2;
​
//让字符型变量变得像整型一样
public class CharDemo2 {
    public static void main(String[] args) {
        char ch1;
        
        ch1='X';
        System.out.println("ch1 contains "+ch1);
        ch1++;//让ch1自增加
        
        System.out.println("ch1 is now "+ch1);
    }
}

这个程序的输出为:

ch1 contains X
ch1 is now Y

在这个程序中,ch1首先被赋值为X。接下来,ch1被加了一个单位。这导致ch1变成了ASCII(和Unicode)序列中的下一个字符Y。

boolean

Java有一个用于表示逻辑值的基本类型,称为布尔类型(boolean),它只能有两个可能的值,true或false。所有关系运算符(如a < b)都返回这种类型。布尔类型也是控制语句(如if和for)所需的条件表达式的类型。我们将在后面的章节介绍控制语句。

2.3 变量的命名

在Java中,变量名只能包含字母、数字、下划线(*)或美元符号()。然而,第一个字符不能是数字。因此,您可以将变量命名userName)。然而,第一个字符不能是数字。因此,您可以将变量命名为_userName、username、username或userName2,但不能是2userName。

然而,我们总是习惯以字母开头命名变量,而不是"$"或"*"。此外,美元符号字符在命名变量时几乎从不使用(虽然在技术上使用它并不是错误的)。变量名称应该简短而有意义,旨在向读者表明其使用的意图。将变量命名为userName、userAge和userNumber,而不是n、a和un。 此外,有一些保留字在Java中已经预先分配了含义,因此不能将其用作变量名。这些保留字包括System、if、while等。我们将在后续章节中学习每个保留字的含义。 在Java中,命名变量时通常使用驼峰式命名法。驼峰式命名法是将复合词以混合大小写的方式书写,除第一个单词外,每个单词的第一个字母大写(例如,thisIsAVariableName)。这是这份笔记中将要使用的约定。 最后,变量名区分大小写。thisIsAVariableName和thisisavariablename不是同一个变量名。

在Java中,所有变量在使用之前都必须声明。变量声明的基本形式如下:

type identifier [ = value ][, identifier [= value ] …];

这里,type 是Java的原子类型之一,或者是一个类或接口的名称。(类和接口类型在本书的后面部分讨论。) identifier 是变量的名称。要声明指定类型的多个变量,请使用逗号分隔的列表。下面是变量声明的例子:

// 声明浮点变量
float simpleInterest; 
​
// 声明多个整型变量
int time ,speed; 
​
// 声明字符型变量
char var ; 

2.4 变量的初始化

在讲述变量的声明的时候,我们在格式中给出[ = value ][, identifier [= value ] …];这就是变量的初始化操作,每次声明一个新的变量,你都需要给它一个初始值。这被称为初始化变量。你可以在程序中稍后更改变量的值。 有两种方法可以初始化变量。你可以在声明点初始化它,或者在一个单独的语句中初始化它。Java还允许使用在变量声明时有效的任何表达式动态地初始化变量。如下所示:

// 展示不同的初始化方法
class Dyninit {
  public static void main(String args[]) {
      double a= 3.0, b = 4.0,d;
      d=5.0;//在之后的语句初始化
     // c 是动态声明
     double c = Math.sqrt (a *a+ b * b);
     System.out.println("d is"+d);
     System.out.println( "Hypotenuse is"+ c ) ;
                           }
     }

2.5 变量的作用域和生命周期

到目前为止,所有使用的变量都在 main() 方法的开头声明。然而,Java 允许在任何块内声明变量。如之前所述,一个块以左花括号开头,右花括号结束。一个块定义了一个作用域。因此,每次开始一个新块时,都会创建一个新的作用域。作用域确定了程序其他部分可见的对象,也确定了这些对象的生命周期。

通常可以按照全局和局部两种一般范畴来考虑作用域。然而,这些传统作用域的分类方法不适用于 Java 的面向对象模型。虽然可以创建相当于全局作用域的变量,但这在Java中几乎用不到。在 Java 中,两个主要的作用域是由类和方法定义的。现在,我们只考虑查方法内定义的作用域。类的变量定义将会在之后详细讨论。

方法定义的变量的作用域从它的左花括号开始。然而,如果该方法有参数,那么它们也包含在方法的作用域内。一个方法的作用域以其右花括号结束。这段代码被称为方法体。

一般来说,在一个作用域内声明的变量对于在该作用域之外定义的代码是不可见的(也就是不可访问的)。因此,当您在作用域内声明一个变量时,您正在将该变量本地化并保护它免受未经授权的访问和/或修改。这种规则为封装提供了基础。在块内声明的变量称为局部变量。

作用域可以嵌套。例如,每次创建一段代码块时,都会创建一个新的嵌套作用域。当发生这种情况时,外部作用域包含内部作用域。这意味着在外部作用域中声明的对象将对内部作用域中的代码可见。然而,反之则不成立。在内部作用域中声明的对象将不可见于外部作用域。

为了理解嵌套作用域的效果,请考虑以下程序:

package com.mycompany.scope;
​
//展现模块的作用域
public class Scope {
​
    public static void main(String[] args) {
        int x;//这个变量的声明在main的所有区域都是有效的
        
        x=10;
        if(x==10)
        {
            int y=20;//仅在这个花括号模块里有效
            //x,y在这里都有效
            System.out.println("x and y "+x+" "+y);
            x=y*2;
        
        }
        //y=100//这里会报错
        //x在这里依然有效
        System.out.println("x is "+x);
        
    }
}
​

正如注释所示,变量x在main()方法的开头声明,并且在main()方法内的所有后续代码中都可以访问。在if块中,变量y被声明。由于块定义了作用域,因此变量y只能在其块中的其他代码中可见。这就是为什么在其块外部,y = 100; 这一行是被注释掉的。如果删除注释符号,编译时会出现错误,因为y在其块外部不可见。在if块内,变量x可以使用,因为块内的代码(即嵌套作用域)可以访问由外层作用域声明的变量。

在块内,变量可以在任何时候声明,但只有在声明后才有效。因此,如果你在方法的开始处定义一个变量,它将对该方法内的所有代码可用。相反,如果你在块的末尾声明一个变量,它实际上是无用的,因为没有代码可以访问它。例如,这个片段是无效的,因为在声明之前无法使用count变量:

// 这段代码是错误的
count = 100; //不能在声明之前赋值
int count;

这里有另一个需要记住的重要点:变量在进入其作用域时创建,在离开其作用域时销毁。这意味着变量一旦超出作用域,就不会保持其值。因此,在方法中声明的变量将不会在对该方法的调用之间保留其值。此外,在块中声明的变量将在离开该块时失去其值。因此,变量的生存期限定于其作用域内

如果变量声明包含在一个循环的初始化程序,则每次进入声明该变量的块时,该变量都将被重新初始化。例如下面这个程序:

package com.mycompany.lifetime;
​
//展示变量的生命周期
public class LifeTime {
    public static void main(String[] args) {
        int x;
        for(x=0;x<3;x++)
        {
            int y=-1;//y在每次循环都被初始化
            System.out.println("y is "+y);
            y=100;
            System.out.println("y is now "+y);
        }
    }
}
​

它的输出结果为:

y is -1
y is now 100
y is -1
y is now 100
y is -1
y is now 100

正如您所看到的,每次进入内部for循环时,y都会被重新初始化为-1。尽管它随后被赋值为100,但这个值被丢失了。最后一点:尽管块可以嵌套,但您不能声明一个变量具有与外部作用域中的变量相同的名称。例如,以下程序是不合法的:

//下面这段代码是无法通过编译的
class ScopeErr{
    public static void main(String srgs[]){
        int bar=1;
        {
            int bar=2;//在子作用域内声明了一个和父作用域一样名称的变量,这样是行不通的。
        }
    }
}
​

2.6 数组

数组是一组同类型的变量,由一个共同的名称引用。任何类型的数组都可以被创建,并且可以具有一个或多个维度。可以通过它的索引访问数组中的特定元素。数组提供了一种方便的方式来组合相关信息。你可以声明一个数组变量,如 numbers[100] 来代替直接声明 100 个独立变量 number0,number1,....,number99。

2.6.1 一维数组

一维数组本质上是一组具有相同类型的变量列表。要创建一个数组,首先必须创建一个所需类型的数组变量。一维数组声明的一般形式如下:

type var-name[ ];

在这里,type声明了数组的原始数据类型。type确定了组成数组的每个元素的数据类型。因此,数组的元素类型决定了数组将保存的数据类型。例如,以下声明了一个名为month_days的“整型数组”:

int month_days[];

尽管此声明建立了month_days一个数组变量,但实际上在物理内存里并不存在这个数组。要实现给这个一维数组分配内存,必须将month_days与一个实际的物理整数数组链接起来,这个物理整数告诉程序在内存中所开辟的大小。具体地,我们将使用new分配一个空间并将其分配给month_days。new是一个专门分配内存的运算符。

array-var = new type [size];

您将在后面的章节中更仔细地了解new,但您现在需要使用它来为数组分配内存。在这里,type指定正在分配的数据类型,size指定数组中的元素数,array-var是链接到数组的数组变量。也就是说,要使用new来分配数组,必须指定要分配的元素类型和数量。new分配的数组中的元素将自动初始化为零(对于int类型)、false(对于布尔类型)或null(对于引用类型,在后面的章节中描述)。

下面示例分配了一个包含12个元素的整数数组,并将其链接到month_days:

month_days = new int[12];

执行完这条语句后,month_days将指向一个包含12个整数的数组。 此外,数组中的所有元素都将初始化为零。

让我们来总结一下:获取一个数组是一个两步过程。首先,您必须声明所需数组类型的变量。其次,您必须使用new分配将保存数组的内存,并将其赋值给数组变量。 因此,在Java中,所有数组都是动态分配的。如果动态分配的概念对您来说不熟悉,不用担心。本书后面会详细介绍它。 一旦您分配了一个数组,就可以通过在方括号内指定其索引来访问数组中的特定元素。所有数组索引都从零开始。例如,此语句将值28分配给month_days的第二个元素:

month_days[1] = 28;

下面一行代码的作用是打印索引为3的元素的值:

System.out.println(month_days[3]);

下面的语句首先声明了一个数组变量 myList,接着创建了一个包含 10 个 double 类型元素的数组,并且把它的引用赋值给 myList 变量。请注意,在编程中,索引从零开始而不是1。这是几乎所有编程语言(如Python和Java)都采用的常见做法。

public class TestArray {
   public static void main(String[] args) {
      // 数组大小
      int size = 10;
      // 定义数组
      double[] myList = new double[size];
      myList[0] = 5.6;
      myList[1] = 4.5;
      myList[2] = 3.3;
      myList[3] = 13.2;
      myList[4] = 4.0;
      myList[5] = 34.33;
      myList[6] = 34.0;
      myList[7] = 45.45;
      myList[8] = 99.993;
      myList[9] = 11123;
      // 计算所有元素的总和
      double total = 0;
      for (int i = 0; i < size; i++) {
         total += myList[i];
      }
      System.out.println("总和为: " + total);
   }
}

以上实例输出结果为:

总和为: 11367.373

下面的图片描绘了数组 myList。这里 myList 数组里有 10 个 double 元素,它的下标从 0 到 9。

java数组结构说明

这是您通常会在专业编写的Java程序中看到的方式。

数组可以在声明时进行初始化。该过程与初始化简单类型的过程非常相似。数组初始化器可以由花括号括起来的逗号分隔的表达式列表来完成,这个过程用逗号分隔数组元素的值。数组将自动创建足够大以容纳数组初始化器中指定的元素数量。不需要使用new。例如,为了存储每个月的天数,以下代码创建了一个已初始化的整数数组:

package com.mycompany.autoarray;
​
//以花括号列表的形式实现的一维数组
public class AutoArray {
    public static void main(String[] args) {
        int month_days[]={31,29,31,30,31,30,31,31,30,31,30,31};
        System.out.println("五月有 "+month_days[4]+" 天");
    }
}
​

Java会严格检查索引是否在数组的范围之外。Java运行时系统将检查所有数组索引是否在正确的范围内。例如,在上面的例子中JRE统将检查每个索引是否在month_days所规定的范围0到11之间(包括0和11)。如果您尝试访问数组范围之外的元素(负数或大于数组长度的数字),则会引发运行时错误(Runtime Error)。

下面的例子展示了如何创建、初始化和操纵数组:

public class TestArray {
   public static void main(String[] args) {
      double[] myList = {1.9, 2.9, 3.4, 3.5};
 
      // 打印所有数组元素
      for (int i = 0; i < myList.length; i++) {
         System.out.println(myList[i] + " ");
      }
      // 计算所有元素的总和
      double total = 0;
      for (int i = 0; i < myList.length; i++) {
         total += myList[i];
      }
      System.out.println("Total is " + total);
      // 查找最大元素
      double max = myList[0];
      for (int i = 1; i < myList.length; i++) {
         if (myList[i] > max) max = myList[i];
      }
      System.out.println("Max is " + max);
   }
}

2.6.2 多维数组

在Java中,多维数组被实现为数组的数组。要声明一个多维数组变量,需要使用另一组方括号指定每个附加索引。例如,以下代码声明了一个名为twoD的二维数组变量:

int twoD[][] = new int[4][5];

它是一个二维数组,最多可以容纳20个元素,需要一个4\times5的整数存储空间,如下图所示:

image-20230407113005226.png

以下程序将从左到右、从上到下的顺序为数组中的每个元素进行编号,然后显示这些值。

package com.mycompany.twodarray;
​
​
public class TwoDArray {
    public static void main(String[] args) {
        int twoD[][]=new int [4][5];
        int i,j,k=0;
        
        for(i=0;i<4;i++)
        {
            for(j=0;j<5;j++)
            {
                twoD[i][j]=k;
                k++;
            }
        }
        
        for(i=0;i<4;i++)
        {
            for(j=0;j<5;j++)
            {
                System.out.print(twoD[i][j]+" ");
            }
            System.out.println();
        }
    }
}
​

这个程序的输出为:

0 1 2 3 4 
5 6 7 8 9 
10 11 12 13 14 
15 16 17 18 19 

当您为多维数组分配内存时,只需要指定第一个(最左边)维度的内存。您可以单独分配其余的维度。例如,以下代码在声明twoD时为第一个维度分配内存,然后单独分配第二个维度的内存。

int twoD[][] = new int[4][];
twoD[0] = new int[5];
twoD[1] = new int[5];
twoD[2] = new int[5];
twoD[3] = new int[5];

咋一看这种方式十分多余,但是在一些其他的情况下这种方式是十分巧妙的,当您单独分配维度时,您不需要为每个维度分配相同数量的元素。由于多维数组实际上是数组的数组,因此每个数组的长度都由您控制。例如,以下程序创建一个二维数组,其中第二个维度的大小不同:

package com.mycompany.twodagain;
​
//手动给第二个维度设置不同的大小
public class TwoDAgain {
    public static void main(String[] args) {
        int twoD[][]=new int [4][];
        twoD[0]=new int[1];
        twoD[1]=new int[2];
        twoD[2]=new int[3];
        twoD[3]=new int[4];
        int i,j,k=0;
        for(i=0;i<4;i++)
        {
            for(j=0;j<i+1;j++)
            {
                twoD[i][j]=k;
                k++;
            }
        }
        for(i=0;i<4;i++)
        {
            for(j=0;j<i+1;j++)
            {
                System.out.print(twoD[i][j]+" ");
            }
            System.out.println();
        }
    }
}
​

这个二维数组的存储方式如下:

image-20230407121256637.png

在某些情况下,不规则数组可以有效地使用。例如,如果需要一个非常大且稀疏的二维数组(即并非所有元素都会被使用),那么不规则数组可能是一个完美的解决方案。

多维数组的初始化也可以用大括号在对应的位置上赋值的方法:

package com.mycompany.matrix;
​
//初始化一个二维数组
public class Matrix {
​
    public static void main(String[] args) {
        double m[][]={
            {0*0,1*0,2*0,3*0,4*0},
            {0*1,1*1,2*1,3*1,4*1},
            {0*2,1*2,2*2,3*2,4*2},
            {0*3,1*3,2*3,3*3,4*3},
        };
        int i,j;
        
        for(i=0;i<4;i++)
        {
            for(j=0;j<4;j++)
            {
                System.out.print(m[i][j]+" ");
            }
            System.out.println();
        }
    }
}
​

这个程序的输出为:

0.0 0.0 0.0 0.0
0.0 1.0 2.0 3.0
0.0 2.0 4.0 6.0
0.0 3.0 6.0 9.0

2.6.3 其他声明数组的方法

还有一种可以用来声明数组的方法:

type[ ] var-name;

在这里,跟随类型说明符的是方括号而不是数组变量的名称。例如,以下两个声明是等价的:

int al[] = new int[3];
int[] a2 = new int[3];

同理下面两种方式也是等价的:

char twod1[][] = new char[3][4];
char[][] twod2 = new char[3][4];

这种声明形式在同时声明多个数组时很方便。例如:

int[] nums, nums2, nums3; // 创建三个数组

上面的语句创建了三个数组,它和以下的方式是等价的:

int nums[], nums2[], nums3[];

2.6.4 Array 方法

与我们在上一章中学习的8种基本类型不同,数组实际上是一个对象。具体来说,它是Array类的一个对象。

如果你不理解这是什么意思,不用担心,我们会在后面中讨论类和对象。你只需要知道Array类为我们提供了许多预先写好的方法,我们在处理数组时可以使用这些方法。方法是可重用代码块,用于执行特定任务。稍后我们会看一些例子。

在Java中,一个方法可能有不同的变体。下面的大多数例子只讨论了每个方法的一种变体。但是,如果你学会了如何使用其中一种变体,就可以相对容易地弄清如何使用其他变体。现在让我们看一些常用的Array方法。我们下面讨论的方法在java.util.Arrays类中找到。要使用它们,您必须添加这个语句:

import java.util.Arrays;

这是告诉编译器在哪里找到这些方法的代码。这条导入语句必须出现在包语句之后,类声明之前。下面是一个示例:

package helloworld;
import java.util.Arrays;
public class HelloWorld {
//Code for HelloWorld class
}

好了,现在我们将具体讲述一些常用的Array方法:

equals()

equals()方法用于确定两个数组是否相等。如果数组相等则返回true,否则返回false。如果两个数组具有相同数量的元素,并且这些元素排列顺序相同,则认为它们是相等的。

假设我们有如下的代码块:

int[] arr1 = {0,2,4,6,8,10};
int[] arr2 = {0,2,4,6,8,10};
int[] arr3 = {10,8,6,4,2,0};
boolean result1 = Arrays.equals(arr1, arr2);
boolean result2 = Arrays.equals(arr1, arr3);

result1的值为true,而result2的值为false。这是因为对于result2,即使arr1和arr3具有相同的元素,但这些元素的排列顺序不同。因此,这两个数组不被认为是相等的。 请注意,在上面的示例中,我们在方法名前面添加了Arrays一词。这是因为Arrays类中的所有方法都是静态的。要调用静态方法,必须在前面加上类的名称。我们将在后面的章节更多地讨论静态方法。f

copyOfRange()

copyOfRange()方法允许您将一个数组的内容复制到另一个数组中。它需要三个参数。 假设您有一个数组:

int [] source = {12, 1, 5, -2, 16, 14, 18, 20, 25};

您可以使用以下语句将源数组的内容复制到新数组dest中:

int[] dest = Arrays.copyOfRange(source, 3, 7);

第一个参数(source)是提供要复制的值的数组。 第二个和第三个参数告诉编译器在哪个索引处开始和停止复制。换句话说,在我们的例子中,我们正在从索引3复制到索引6(即不复制索引7处的元素)。 复制元素后,copyOfRange()方法返回一个带有复制的数字的数组。然后将该数组分配给dest。 因此,dest变为{-2, 16, 14, 18},而source保持不变。

toString()

toString()方法返回表示数组内容的字符串。这使我们可以轻松地显示数组的内容。例如,假设您有一个int[]类型的数组:

int[] numbers = {1, 2, 3, 4, 5};

您可以使用以下语句来显示numbers的内容。

System.out.println(Arrays.toString(numbers));

您将获得以下输出:

[1, 2, 3, 4, 5]

sort()

sort()方法可用于对数组中的元素进行排序。此方法可接受一个数组作为参数,并使用快速排序算法将元素按升序排序。使用该方法需要注意以下几点:

  1. sort()方法只能对具有自然排序顺序的元素(如整数和浮点数)进行排序。对于其他类型的元素(如字符是没有顺序的),需要通过实现Comparable接口来自定义排序规则。
  2. sort()方法可以对任何类型的数组进行排序,包括基本类型数组和对象类型数组。
  3. sort()方法可以通过指定比较器(Comparator)来自定义排序规则。

下面是一个使用sort()方法对整数数组进行排序的例子:

int[] arr = {5, 2, 8, 1, 9};
Arrays.sort(arr);
System.out.println(Arrays.toString(arr));

输出结果为:[1, 2, 5, 8, 9]

在这个例子中,我们首先定义了一个整数数组arr,并将一些随机的整数赋值给它。然后,我们调用Arrays类中的sort()方法,对数组中的元素进行升序排序。最后,使用Arrays类的toString()方法将排序后的数组打印出来。

sort()方法可以通过指定比较器(Comparator)来自定义排序规则,比较器是一个实现了Comparator接口的类。该接口定义了一个compare()方法,用于比较两个元素的大小。通过实现该接口的compare()方法,可以自定义排序规则。

String[] arr = {"apple", "banana", "orange", "pear", "watermelon"};
Arrays.sort(arr, new Comparator<String>() {
    public int compare(String s1, String s2) {
        return Integer.compare(s1.length(), s2.length());
    }
});
System.out.println(Arrays.toString(arr));
​

输出结果为:[pear, apple, banana, orange, watermelon]

在这个例子中,我们定义了一个字符串数组arr,并赋值一些字符串。然后,我们使用Arrays类的sort()方法进行排序,同时指定了一个比较器来自定义排序规则。该比较器是一个实现了Comparator接口的匿名内部类,它的compare()方法根据字符串长度来比较两个字符串的大小。最后,使用Arrays类的toString()方法将排序后的数组打印出来。

需要注意的是,在比较器中,如果s1小于s2,则compare()方法应该返回负整数;如果s1等于s2,则返回0;如果s1大于s2,则返回正整数。

如果对于比较器不太理解,不要紧,我们在后面的章节详细讨论。

binarySearch()

binarySearch()方法允许你在一个有序数组中搜索特定的值。要使用这个方法,请先确保你的数组已经排序。你可以使用上面提到的sort()方法进行排序。

假设我们有以下数组:

int[] myInt = {21, 23, 34, 45, 56, 78, 99};

为了确定78是否在数组中,我们写了如下程序:

int foundIndex = Arrays.binarySearch(myInt, 78);

foundIndex将等于5。这表示数字78在索引5处找到。

另一方面,如果你写下:

int foundIndex2 = Arrays.binarySearch(myInt, 39);

foundIndex2将等于-4。这个结果有两部分 - 负号和数字4。 负号只是表示39没有找到。另一方面,数字4有点奇怪。它告诉你如果它存在,那么它应该在数组中哪里。但是,它会将应该存在的这个位置的索引加1。

2.6.5 数组长度

最后,让我们看一下如何得到数组的长度。数组的长度告诉我们数组中的元素数量。我们使用length字段得到数组的长度。例如,如果我们有

int [] userAge = {21, 22, 26, 32, 40}; 

userAge.length等于5,因为数组中有5个数字。

2.7 String导论

在前面的数据类型和数组讨论中,您可能已经注意到没有提到字符串或字符串数据类型。这并不是因为Java不支持这样的类型 - 实际上它是支持的。只是Java的字符串类型String不是原始类型,也不是仅仅是字符数组。相反,String定义了一个对象,对其的完整描述需要理解几个与对象相关的特性。在本书的后面介绍对象后再进行讨论。这里我们先简单地介绍一下String

首先,让我们看一下字符串。字符串是一段由字符组成的文本,例如“Hello World”或“Good morning”。 要声明并初始化一个字符串变量,您可以写如下代码:

String message = "Hello World";

其中,message是字符串变量的名称,“Hello World”是分配给它的字符串。请注意,您需要将字符串括在双引号(“)中。 您也可以将空字符串分配给字符串变量,如下所示:

String anotherMessage = "";

如果您想将两个或多个字符串连接在一起,可以使用连接符(+)。例如,您可以写成:

String myName = "Hello World, " + "my name is Jamie";

这与以下代码是相同的:

String myName = "Hello World, my name is Jamie";

2.7.1 String方法

和Array一样,String也有一些预先写好的方法,下面我们来介绍:

length()

length()方法告诉我们字符串中字符的总数。要找到字符串“Hello World”的长度,我们写下

“Hello World”.length();

length()方法返回字符串的长度。您可以将此结果赋值给变量,如下所示。

int myLength = "Hello World".length();

在上面的示例中,myLength将等于11,因为“Hello”和“World”都有5个字符。当你在两个单词之间加上空格时,你得到一个长度为11的字符串。

您可以使用以下语句显示length()方法的结果。

int myLength = "Hello World".length();
System.out.println(myLength);

toUpperCase()/toLowerCase()

toUpperCase() 方法用于将字符串转换为大写字符。toLowerCase() 方法用于将字符串转换为小写字符。例如,要将字符串“Hello World”转换为大写,我们可以编写:

String uCase = "Hello World".toUpperCase();

在语句的右侧,我们使用字符串“Hello World”调用 toUpperCase() 方法。然后我们将结果赋给变量 uCase。 因此,uCase 将等于“HELLO WORLD”。

substring()

substring() 方法用于从较长的字符串中提取子字符串。例如,要从“Hello World”中提取一个子字符串,可以使用以下语句:

String firstSubstring = "Hello World".substring(6);

在语句的右侧,我们使用“Hello World”字符串调用 substring() 方法。括号中的数字6是称为参数的值。此参数告诉编译器从哪里开始提取子字符串。本质上,它要求编译器从索引6(即位置6)开始提取子字符串,一直提取到字符串的结尾。上述语句将提取子字符串“World”。然后将此结果赋给 firstSubstring。 因此,firstSubstring 等于 “World”。

substring() 方法还有另一种变体,允许我们从一个索引提取子字符串到另一个索引。假设您想从位置1到7提取子字符串,可以按以下方式操作:

String message = "Hello World";
String secondSubstring = message.substring(1, 8);

在上面的示例中,我们首先将“Hello World”分配给变量 message。然后使用 message 调用 substring() 方法。两个参数分别是 1 和 8。 与之前一样,第一个参数告诉编译器要提取的起始位置的索引。第二个参数告诉编译器要停止提取的第一个位置的索引。换句话说,在我们的例子中,编译器在第8个位置停止提取(而不是在第8个位置之后)。这意味着不包括位置8处的字母在子字符串中。因此,提取的子字符串为“ello Wo”。

charAt()

charAt()方法返回指定位置的单个字符。然后可以将此字符分配给char变量。

例如,语句

char myChar = "Hello World".charAt(1);

提取索引为1的字符并将其分配给myChar。因此,myChar等于“e”。

equals()

equals()方法用于比较两个字符串是否相同。如果字符串相等,它将返回true,如果它们不相等,则返回false。

如果我们有以下语句:

boolean equalsOrNot = "This is Jamie".equals("This is Jamie"); 
​
boolean equalsOrNot2 = "This is Jamie".equals("Hello World"); 

equalsOrNot将为true,而equalsOrNot2将为false。

split()

split() 方法根据用户定义的分隔符(也称为定界符)将字符串拆分为子字符串。拆分字符串后,split() 方法返回一个包含结果子字符串的数组。例如,要将字符串拆分为子字符串,您可以按如下方式编写代码:

String names = "Peter, John, Andy, David";
String[] splitNames = names.split(", ");

在此,我们首先将要拆分的字符串赋值给变量 names。然后使用 names 调用 split() 方法。split() 方法接受一个参数 - 用于分隔子字符串的定界符。在我们的示例中,定界符是一个逗号后跟一个空格。 上面代码的结果是以下数组: {"Peter", "John", "Andy", "David"} 这个数组被分配给变量 splitNames。 我们已经介绍了 Java 中常用的一些 String 方法。有关所有可用 String 方法的完整列表,请查看此页面

docs.oracle.com/javase/8/do…

2.8 原始数据类型和引用数据类型

现在我们熟悉了Java中的字符串和数组,让我们讨论一个关于数据类型的重要概念。 在Java中,所有数据类型都可以被分类为原始类型或引用类型。Java只有8种原始类型(byte,short,int,long,float,double,char和boolean),其余都是引用类型。引用类型的例子包括本章中讨论的字符串和数组,以及后面章节中将要讨论的类和接口。 原始类型和引用类型之间的主要区别之一是存储的数据。 原始类型存储它自己的数据。 当我们写下

int myNumber = 5;

变量myNumber存储实际值5。 另一方面,引用类型不存储实际数据。相反,它存储对数据的引用。它不告诉编译器数据的值是什么;它告诉编译器在哪里找到实际数据。 引用类型的一个例子是字符串。当你写出类似于

String message = "Hello";

时,变量message实际上并不存储字符串“Hello”。 相反,“Hello”字符串在计算机内存中的其他地方创建并存储。变量message存储该内存位置的地址。注意原始类型和引用类型之间存在差异;前者存储值,而后者存储地址。

2.9 类型转换和强制类型转换

如果你有编程经验,那么你已经知道将一个类型的值赋给另一个类型的变量是相当常见的。如果这两种类型是兼容的,Java 将自动执行转换。例如,将 int 值赋给 long 变量总是可以的。然而,并非所有类型都是兼容的,因此,并非所有类型转换都被隐式允许。例如,从 double 到 byte 没有定义自动转换。幸运的是,我们仍然可以获取不兼容类型之间的转换。为此,您必须强制使用转换,它执行不兼容类型之间的显式转换。让我们看看自动类型转换和强制类型转换。

2.9.1 Java的自动类型转换

一个数据类型被赋值给另一个变量时,如果满足以下两个条件,则会发生自动类型转换:

  • 两种类型是兼容的。
  • 目标类型所能表示的范围大于源类型所能表示的范围。

当满足这两个条件时,会进行扩展转换。例如,int 类型始终足够大以容纳所有有效的 byte 值,因此不需要显式转换语句。 对于扩展转换,数字类型,包括整数和浮点类型,彼此之间是兼容的。然而,从数字类型到 char 或 boolean 没有自动转换。另外,char 和 boolean 也彼此之间不兼容。

2.9.2 强制不兼容类型的转换

尽管自动类型转换很有用,但它们并不能满足所有需求。例如,如果您想将int值赋给byte变量,该怎么办?这种转换不会自动执行,因为byte所能表示的范围比int小。这种转换有时被称为缩小转换,因为您明确地使值变窄,以使其适合目标类型。 要创建两种不兼容类型之间的转换,必须使用强制转换。强制转换就是显式的类型转换。它的一般形式如下:

 (目标类型) 值 

这里,目标类型指定要将指定的值转换为的所需类型。 例如,以下片段将int强制转换为byte。如果整数的值大于byte的范围,则它将对byte的范围取模(整数除法的余数)

int a;
byte b;
// …
b = (byte) a;

当将浮点值赋给整数类型时,会发生不同类型的转换:截断。如您所知,整数没有小数部分。因此,当将浮点值赋给整数类型时,小数部分会丢失。例如,如果将值1.23赋给整数,则结果值将简单地为1。0.23将已被截断。当然,如果整数组件的大小太大而无法适合目标整数类型,则该值将对目标类型的范围取模。 以下程序演示了一些需要使用强制转换的类型转换:

package com.mycompany.conversion;
​
//展示类型转换
public class Conversion {
    public static void main(String[] args) {
        byte b;
        int i=257;
        double d=323.142;
        
        System.out.println("\n将int类型转换为byte");
        b=(byte) i;
        System.out.println("i 和 b分别为"+i+" "+b);
        
        System.out.println("\n将double类型转换为int");
        i=(int) d;
        System.out.println("d 和 i分别为"+d+" "+i);
        
        System.out.println("\n将double类型转换为byte");
        b=(byte) d;
        System.out.println("d 和 i分别为"+d+" "+b);
    }
}

这个程序的输出为:

int类型转换为byte
i 和 b分别为257 1
​
将double类型转换为int
d 和 i分别为323.142 323
​
将double类型转换为byte
d 和 i分别为323.142 67

让我们来看看每个转换。当将值257转换为byte变量时,结果是将257除以256(byte的范围)所得的余数,在本例中为1。将d转换为int时,其小数部分会丢失。当将d转换为byte时,其小数部分会丢失,并将该值对256取模,而在这种情况下结果为67。

2.9.3 在表达式中的自动类型升级

除了赋值,还有另一个地方可能发生某些类型转换:表达式。为了了解为什么会这样,考虑以下情况。在表达式中,有时中间值的精度超出了任一操作数的范围。例如,看一下以下表达式:

byte a = 40;
byte b = 50;
byte c = 100;
int d = a * b / c;

中间项 a * b 的结果很容易超出其任一字节操作数的范围。为了处理这种问题,Java 在求值表达式时会自动将每个字节、短整型或字符型操作数提升为 int。这意味着子表达式 a * b 使用的是整数,而不是字节。因此,中间表达式 50 * 40 的结果是 2,000,即使 a 和 b 都被指定为 byte 类型,这也是合法的。

尽管自动提升非常有用,但它们可能会导致令人困惑的编译时错误。例如,这段看似正确的代码会导致问题:

byte b = 50;
b = b * 2; // 错误!不能将 int 分配给 byte!

该代码试图将 50 * 2(一个完全有效的 byte 值)存储回一个 byte 变量中。然而,因为在求值表达式时操作数被自动提升为 int,所以结果也被提升为 int。因此,表达式的结果现在是 int 类型,而不使用强制类型转换就无法将 int 分配给 byte。即使在这种特殊情况下被分配的值仍适合目标类型,这也是正确的。

在了解溢出后果的情况下,应该使用显式类型转换,例如:

byte b = 50;
b = (byte)(b * 2);

这将产生正确的值 100。

2.9.4 类型升级的规则

Java 定义了几个适用于表达式的类型提升规则。它们如下:首先,所有的 byte、short 和 char 值都会像刚才描述的那样被提升为 int。然后,如果一个操作数是 long,则整个表达式都会被提升为 long。如果一个操作数是 float,则整个表达式都会被提升为 float。如果任何操作数都是 double,则结果是 double。

下面的程序演示了如何将表达式中的每个值提升为与每个二元运算符的第二个参数匹配的类型:

package com.mycompany.promote;
​
​
public class Promote {
    public static void main(String[] args) {
        byte b=42;
        char c='a';
        short s=1024;
        int i=5000;
        float f=5.76f;
        double d=.1234;
        double result=(f*b)+(i/c)-d*s;
        System.out.println((f*b)+"+"+(i/c)+"-"+(d*s));
        System.out.println("result ="+ result);
    }
}

让我们仔细看一下程序中出现的类型提升,特别是这一行:

double result = (f * b) + (i / c) - (d * s);

在第一个子表达式f * b中,b被提升为float类型,子表达式的结果是float类型。接下来,在子表达式i/c中,c被提升为int类型,结果是int类型。然后,在d * s中,s的值被提升为double类型,子表达式的类型是double。最后,这三个中间值,float、int和double,被考虑在内。float加int的结果是float。然后将所得的float减去最后一个double,并提升为double,这是表达式最终结果的类型。

2.10 引入局部变量类型推断

最近,Java语言新增了一项令人兴奋的新特性,称为局部变量类型推断。让我们回顾一下变量的两个重要方面。首先,Java中的所有变量必须在使用之前声明。其次,变量可以在声明时初始化一个值。今天,这种情况已经发生了改变。 从JDK 10开始,现在可以让编译器推断局部变量的类型。

要使用局部变量类型推断,必须使用var作为类型名进行变量声明,并包含初始化器。例如,以前您会声明一个名为avg的本地double变量,并将其初始化为值10.0,如下所示:

double avg = 10.0;

使用类型推断,此声明现在也可以像这样编写:

var avg = 10.0;

在两种情况下,avg的类型都是double。在第一种情况下,它的类型是显式指定的。在第二种情况下,它的类型被推断为double,因为初始化器10.0的类型是double。 如前所述,var被添加为上下文敏感的标识符。当它用作本地变量声明上下文中的类型名称时,它告诉编译器使用类型推断来确定正在声明的变量的类型,基于初始化器的类型。因此,在本地变量声明中,var是实际推断类型的占位符。但是,在大多数其他地方使用var时,它只是一个用户定义的标识符,没有特殊含义。例如,以下声明仍然有效:

int var = 1; // 在这种情况下,var只是一个用户定义的标识符。

在这种情况下,类型被显式指定为int,var是正在声明的变量的名称。尽管它是一个上下文敏感的标识符,但它没有任何特殊含义。var不能作为一个类的名称,下面一个程序我们将演示这种变量推断的机制:

package com.mycompany.vardemo;
​
//对于变量推断的简单示例
public class VarDemo {
​
    public static void main(String[] args) {
        //用推断机制声明变量
        var avg=10.0;
        System.out.println("avg的大小"+avg);
        
        //定义一个变量名字为var
        int var=1;
        System.out.println("var的值为"+var );
        
        //甚至可以用推断机制声明一个var变量
        var k=-var;
        System.out.println("k的值为"+k);
    }
}
​

他的输出为:

avg的大小10.0
var的值为1
k的值为-1

前面的例子仅使用 var 来声明简单的变量,但你也可以使用 var 来声明数组,例如:

var myArray = new int[10]; // 这是合法的。

注意,var 和 myArray 都没有方括号。相反,myArray 的类型被推断为 int[ ]。此外,你不能在 var 声明的左侧使用方括号。因此,下面两个声明都是无效的:

var[] myArray = new int[10]; // 错误 
myArray[] = new int[10]; // 错误

在第一行中,尝试给 var 加上方括号。在第二个声明中,尝试给 myArray 加上方括号。在这两种情况下,使用方括号是错误的,因为类型是从初始值的类型推断出来的。 需要强调的是,只有变量被初始化时,才能使用 var 来声明变量。 例如,下面的语句是错误的:

var counter; // 错误!需要初始化器。

另外,记住 var 只能用于声明局部变量,不能用于声明实例变量、参数或返回类型等。 虽然前面的讨论和例子介绍了局部变量类型推断的基础知识,但还没有展示它的全部功能。如你将在第 7 章中看到的那样,局部变量类型推断在缩短涉及长类名的声明方面特别有效。它也可以与泛型类型、try-with-resources 语句和 for 循环一起使用。

除了前面讨论过的限制外,使用var还有几个其他限制。一次只能声明一个变量;变量不能使用null作为初始化器;正在声明的变量不能被初始化表达式使用。虽然可以使用var声明数组类型,但不能在数组初始化程序中使用var。例如,这是有效的:

var myArray = new int[10]; // 正确。

但是,这个是不正确的:

var myArray = { 1, 2, 3 }; // 错误。

正如前面提到的,var不能用作类的名称。它也不能用作其他引用类型的名称,包括接口、枚举或注释的名称,或用作泛型类型参数的名称,所有这些在本书后面会介绍。这里还有两个与Java功能有关的限制,它们在后续章节中描述,但在这里提到是为了完整性。局部变量类型推断不能用于声明被catch语句捕获的异常类型。此外,lambda表达式和方法引用也不能用作初始化器。

2.11 字面量

在Java中,常量的创建是通过使用其字面值表示的方式实现的。例如,下面是一些字面值的示例:

image-20230409233509674.png

从左到右,第一个字面量指定一个整数,下一个是浮点数,第三个是字符常量,最后一个是字符串。字面量可以在任何允许使用其类型的值的地方使用。

2.11.1 整数字面值

整数是程序中最常用的类型。任何整数值都是一个整数字面值。例如1、2、3和42都是十进制值,表示10进制数。在整数字面值中还可以使用其他两种进制,即八进制(基数为8)和十六进制(基数为16)。Java中的八进制值以前导零表示。普通的十进制数不能有前导零。因此,看似合法的值09会产生编译器错误,因为9超出了八进制的0到7范围。程序员常用的数字基数是十六进制,因为它与模8字长非常匹配,例如8、16、32和64位。用前导0x或0X表示十六进制常量。十六进制数字的范围是0到15,因此用A到F(或a到f)表示10到15。整数字面值创建一个int值,它在Java中是一个32位的整数值。由于Java是强类型的,您可能会想知道如何将整数字面值赋给Java的其他整数类型,如byte或long,而不会导致类型不匹配错误。幸运的是,这种情况很容易处理。当将字面值赋给byte或short变量时,如果字面值在目标类型的范围内,则不会生成错误。整数字面值总是可以分配给long变量。但是,要指定long字面值,您需要明确地告诉编译器字面值的类型是long。通过在字面值后面附加大写或小写的L来实现这一点。例如,0x7ffffffffffffffL或9223372036854775807L是最大的long。只要在范围内,整数也可以分配给char。还可以使用二进制指定整数字面值。要这样做,请在值前面加上0b或0B。例如,以下代码使用二进制字面值指定十进制值10:

 int x = 0b1010; 

除了其他用途之外,添加二进制字面值可以更容易地输入用作位掩码的值。在这种情况下,值的十进制(或十六进制)表示形式无法视觉上传达其相对于其用途的含义,而二进制字面值可以。

您可以在整数字面量中嵌入一个或多个下划线,这样可以更容易地阅读大型整数字面量。当编译字面量时,下划线将被丢弃。例如,给定以下代码:

int x = 123_456_789;

则x的值将为123,456,789。下划线将被忽略。下划线只能用于分隔数字,不能出现在字面量的开头或结尾。然而,在两个数字之间可以使用多个下划线。例如,下面的代码是有效的:

 int x = 123___456___789;

在整数字面量中使用下划线特别有用,例如在编码电话号码、客户ID号码、零件号码等时。在指定二进制字面量时,下划线也很有用。例如,二进制值通常以四位数为一组进行可视化分组,如下所示:

int x = 0b1101_0101_0001_1010;

2.11.2 浮点数字面量

浮点数表示带有小数部分的十进制值。它们可以用标准记数法或科学记数法表示。标准记数法由一个整数部分,后跟一个小数点和一个小数部分组成。例如,2.0、3.14159和0.6667表示有效的标准记数法浮点数。科学记数法使用一个标准记数法的浮点数加上一个后缀,该后缀指定要乘以该数字的10的幂。指数由一个E或e表示,后跟一个小数,可以是正数或负数。例如,6.022E23、314159E-05和2e+100。

Java中的浮点文字默认为双精度。要指定一个浮点文字,必须在常量后附加F或f。您还可以通过附加D或d来显式指定双精度字面量。当然,这样做是多余的。默认的双类型需要64位存储空间,而较小的float类型仅需要32位。

十六进制浮点文字面量也被支持,但它们很少使用。它们必须是类似于科学记数法的形式,但使用P或p而不是E或e。例如,0x12.2P2是一个有效的浮点文字面量。P后面的值称为二进制指数,表示数字乘以的二次幂。因此,0x12.2P2表示72.5。

您可以在浮点文字面量中嵌入一个或多个下划线。这个特性与刚才描述的整数文字面量的工作方式相同。它的目的是使阅读大型浮点文字面量更容易。在编译文字面量时,下划线会被丢弃。例如,给定

double num = 9_423_497_862.0;

给num的值将是9,423,497,862.0。下划线将被忽略。与整数文字面量一样,下划线只能用于分隔数字。它们不能出现在文字面量的开头或结尾。然而,允许在两个数字之间使用多个下划线。

在数字的小数部分中使用下划线也是允许的。例如,

double num = 9_423_497.1_0_9; 

是合法的。在这种情况下,小数部分为0.109。

2.11.3 布尔字面量

布尔字面量很简单。布尔值只有两个逻辑值,即 true 和 false。true 和 false 的值不会转换为任何数值表示。在 Java 中,true 字面量不等于 1,false 字面量也不等于 0。在 Java 中,布尔字面量只能分配给声明为 boolean 的变量或在带布尔运算符的表达式中使用。

2.11.4 字符字面量

Java 中的字符是 Unicode 字符集中的索引。它们是 16 位值,可以转换为整数并使用整数运算符(如加法和减法运算符)进行操作。字面字符表示在一对单引号内。所有可见的 ASCII 字符都可以直接输入到引号内,例如 'a'、'z' 和 '@'。对于不可能直接输入的字符,有几个转义序列允许您输入所需的字符,例如 ' ' ' 表示单引号字符本身,' \n' 表示换行符。还有一种直接输入字符值的机制,可以使用八进制或十六进制。对于八进制表示法,使用反斜杠后跟三位数字。例如,' \141' 是字母 'a'。对于十六进制,您输入反斜杠-u(\u),然后精确输入四个十六进制数字。例如,' \u0061' 是 ISO-Latin-1 'a',因为最高字节为零。' \ua432 ' 是日语片假名字符。下面这张表格显示了字符转义序列。

转义字符意义ASCII码值(十进制)
\b退格(BS) ,将当前位置移到前一列008
\f换页(FF),将当前位置移到下页开头012
\n换行(LF) ,将当前位置移到下一行开头010
\r回车(CR) ,将当前位置移到本行开头013
\t水平制表(HT) (跳到下一个TAB位置)009
\v垂直制表(VT)011
**代表一个反斜线字符'''092
'代表一个单引号(撇号)字符039
"代表一个双引号字符034
\0空字符(NULL)000
\ddd1到3位八进制数所代表的任意字符三位八进制
\uhhhh1到2位十六进制所代表的任意字符二位十六进制

2.11.3 String字面量

Java 中的字符串字面量的指定方式与大多数其他语言相同——将一系列字符包含在一对双引号之间。字符串字面量的示例包括

"Hello World"
"two\nlines"
" "This is in quotes""

在字符串字面量中定义的转义序列和八进制/十六进制符号与字符字面量中的使用方式相同。Java 字符串中的一个重要注意点是,它们必须在同一行开始和结束。与其他一些语言中存在的换行续行转义序列不同,Java 中不存在这样的转义序列。

2.12 一些面试题

这里总结一些常见的面试题,如果现在还不是很理解,不要紧,先把本笔记过一遍再回来理解。

  1. Java中有哪些基本数据类型?

答:Java中的基本数据类型有8种,分别是:byte、short、int、long、float、double、char和boolean。

  1. Java中变量的命名规则是什么?

答:Java中变量的命名规则如下:变量名由字母、数字和下划线组成,第一个字符必须是字母或下划线,变量名不允许使用Java中的关键字,变量名是大小写敏感的,建议使用有意义的名称。

  1. 什么是局部变量?

答:在Java中,局部变量是定义在方法、代码块或构造函数内部的变量。局部变量只能在其所在的代码块内访问。

  1. 什么是实例变量?

答:在Java中,实例变量是定义在类中,但在方法外的变量。它们可以在类的任何方法、构造函数或块中使用。它们被分配在对象创建时,并且每个对象都有自己的实例变量副本。

  1. 什么是静态变量?

答:在Java中,静态变量是定义在类中,但在方法外的变量。它们被分配在类加载时,并且在类的任何对象创建之前已经存在。它们只有一个副本,可以由类的任何对象访问。静态变量可以通过类名访问,也可以通过对象名访问。

  1. 什么是常量?

答:在Java中,常量是指不可更改的值,也称为“final变量”。它们可以是基本数据类型,如int和double,也可以是引用类型,如字符串和数组。声明常量时必须使用关键字“final”,一旦赋值,就不能再更改。

  1. 什么是自动装箱和自动拆箱?

答:在Java中,自动装箱是指将基本数据类型转换为其相应的包装器类型的过程,例如将int转换为Integer。自动拆箱是指将包装器类型转换为其相应的基本数据类型的过程,例如将Integer转换为int。这些转换由Java编译器自动完成。

  1. 什么是类型推断?

答:在Java 10及以上版本中,类型推断是指使用var关键字来推断变量类型的过程。它可以简化代码,减少冗余,并帮助处理长类名和难以识别的类型。使用类型推断时,必须在变量声明中使用var作为类型名,并且必须包含一个初始化程序。

  1. 什么是作用域?

答:变量的作用域指的是变量在程序中可以被访问的范围。在Java中,变量的作用域可以分为以下几种:

  • 局部变量作用域:在方法或代码块中定义的变量,只能在其定义的方法或代码块中访问。
  • 成员变量作用域:在类中定义的变量,可以在整个类中的方法和代码块中访问。
  • 静态变量作用域:在类中使用关键字static定义的变量,可以在整个类中的方法和代码块中访问,且可以通过类名直接访问。
  • 参数作用域:方法的参数也可以视为一个变量,其作用域为整个方法体。
  1. 什么是自动类型转换和强制类型转换?它们有什么区别?

答:自动类型转换是指在需要类型兼容的情况下,Java编译器自动将一个类型转换为另一个类型。例如,当一个int类型的变量与一个long类型的变量进行运算时,Java编译器将自动将int类型转换为long类型。强制类型转换是指将一个数据类型强制转换为另一个数据类型。这通常是在需要进行更精细的计算或将一种数据类型转换为另一种数据类型时使用的。强制类型转换需要使用强制转换运算符,即括号。例如,将一个double类型的变量强制转换为int类型,可以使用以下语句:int i = (int) d; 自动类型转换和强制类型转换的主要区别是,自动类型转换是在编译时发生的,而强制类型转换是在运行时发生的。此外,自动类型转换是Java编译器根据Java语言规范自动执行的,而强制类型转换是由程序员显式执行的。

  1. 什么是静态变量和实例变量?它们有什么区别?

答:静态变量是在类级别上定义的变量。它们是类的属性,而不是类的任何实例的属性。静态变量使用static关键字声明,可以在没有创建类的实例的情况下访问。实例变量是在类的实例级别上定义的变量。它们是类的实例的属性,每个类实例都有自己的实例变量。实例变量不需要使用static关键字声明,因为它们已经与类实例相关联。静态变量和实例变量之间的主要区别在于它们的作用范围和生命周期。静态变量的作用域是整个程序,而实例变量的作用域仅限于类的实例。静态变量在程序开始运行时创建,并在程序结束时消失。