Java 基本语法和数据结构

基础知识。

一个 Java 的 HelloWorld 程序

1
2
3
4
5
6
7
public class FirstProgram
{
public static void main(String[] args)
{
System.out.println("Hello World!");
}
}

编译

使用 javac.exe 对源代码进行编译。

javac 的用法是:

1
javac -d destdir srcFile

其中使用 -d 选项指定编译后生成的文件的输出位置。如果不指定 -d 选项,那么默认输出到当前路径。

编译结束之后,javac 程序会为源代码中的每一个类生成一个对应的 .class 文件,文件名为相应的类名。也就是说,如果一个源文件中定义了两个类,那么编译这个源文件会生成两个 .class 文件,对应源代码中的两个类。

运行

使用 java.exe 执行类。

用法是:

1
java 类名

java 后直接输入相应的类名,不带文件名后缀。

使用 java.exe 执行相应的类的时候,要求所执行的类必须有 main 方法,而且 main 方法必须是 public static void main(String[] args) 这样的写法。

关于 CLASSPATH

JDK 1.5 之后的版本可以不用设置 CLASSPATH。JDK 1.5 之后的版本,使用 java 类名 运行程序,java 会默认在当前目录下寻找类文件,但是 JDK 1.5 之前并没有设置这个机制,需要借助于 CLASSPATH 环境变量或者 -classpath 参数才知道去哪里寻找类。所以 JDK 1.5 之后就可以不设置 CLASSPATH 环境变量了,而 JDK 1.5 之前还是需要设置的。

另外,也可以使用 CLASSPATH 来制定一些第三方类库的路径,从而让程序可以找到相应的第三方类库。

命名规则及大小写

  • Java是区分大小写的。对于大小写问题,简单总结如下:
区分大小写的语言 不区分大小写的语言
Java、C/C++、C#、Python 汇编(Assembly)、SQL、Visual Basic

文件名命名规则

一个 .java 文件中至多只能有一个 public 类,Java 源代码文件的文件名必须和源代码中公共类(public类)的名字相同,文件名和公共类的大小写也要保持一致,不然编译时会报错。

标识符命名规则

Java 的标识符必须以字母、下划线(_)、美元符号($)开头,后面可以跟任意数目的字母、数字、下划线和美元符号。此处的字母不仅仅可以是英文字母,还可以是中文字符、日文字符等等 Unicode 字符。ASCII 字符中,除了大小写拉丁字母和下划线和美元符号,其他字符如 @# 不能用于标识符。

Java官方文档中关于标识符的说明:3.8. Identifiers - Chapter 3. Lexical Structure

注释

和 C/C++ 相同,Java支持使用 ///* */ 进行注释。

需要注意的是 /* */ 注释方式不支持嵌套,所以使用时还要必要看具体要注释的代码的情况。如果要注释掉的代码中出现了 /* 或者 */ ,可能会和你添加的注释就近配对,从而达不到你想要的效果。

文档注释

另外还有一种注释方式,这种注释方式以 /** 开头,以 */ 结尾。这种注释形式可以使用 Javadoc 工具自动生成 API 文档。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* This is a useful way of comments
* @version 1.00 2018-12-06
* @author 52Heartz
*/

public class FirstProgram
{
public static void main(String[] args)
{
System.out.println("Hello World!");
}
}

基本数据类型

Java是一种强类型语言,必须为每一个变量显式地声明一种类型。

Java有8种基本数据类型,称为 Primitive type,其中有4种整型、2种浮点类型、1种用于表示Unicode编码的字符单元的字符类型char类型和一种用于表示布尔值的boolean类型。

整型

类型 占用的存储空间大小 取值范围
int 4 Byte -2 147 483 648 ~ 2 147 483 647(正负21亿多,正负10位数)
short 2 Byte -32 768 ~ 32 768
long 8 Byte -9 223 372 036 854 775 808 ~ 9 223 372 036 854 775 807(正负19位数,最高位是9)
byte 1 Byte -128 ~ 127

实际应用情况

  • 通常情况下,int 类型最为常用,如果 int 不够用,就使用 long
  • byteshort 类型主要用于特定场合,例如底层的文件处理或者需要控制占用存储空间量的大数组。

关于基本数据类型,Java和C/C++的区别

在C和C++中,int和long等类型的大小与目标平台相关。在8086这样的16位处理器上整形数值占2字节;不过在32位处理器(比如Pentium或SPARC)上,整形数值则为4字节。类似地,在32位处理器上long值为4字节,在64位处理器上则为8字节。由于存在这些差别,这对编写跨平台程序带来了很大难度。在Java中,所有的数值类型所占据的字节数量与平台无关。

注意,Java没有任何无符号(unsigned)形式的int、long、short、或byte类型。

长整型常量

当表示长整型常量的时候,需要在数字后边加上 L 或者 l ,也就是说需要表示大于2 147 483 647或者小于-2 147 483 648的常数的时候需要在数字后边加上 L 或者 l 。如果-2 147 483 648和2 147 483 647之间就可以不在在数字后边加L。

数字加下划线增强可读性

整型常量太长的时候不利于阅读和识别。从Java7开始可以给数字字面量加下划线来增强可读性。

例如 1_000_000 表示一百万,或者使用中国习惯的表示方法,使用 1_0000_0000 表示一亿。

这些下划线只是为了让人更容易读。Java编译器会去除这些下划线。

表示十六进制常量

数字前加上前缀 0x 或者 0X,比如 Ox123A

表示二进制常量

数字前加上前缀 0b 或者 0B,比如 Ob1101 对应于十进制中的15。

表示八进制常量

数字前面加上前缀 0,比如 010 对应于十进制中的8。

浮点类型

类型 存储需求 取值范围
float 4 Byte 大约±3.402 823 47E+38F(有效位数为6-7位)
double 8 Byte 大约±1.797 693 134 862 315 70E+308(有效位数为15位)

实际应用情况

float一般称为单精度浮点型,double称为双精度浮点型。绝大部分应用程序都采用double类型,在很多情况下,float类型的精度很难满足需求。实际上,只有很少的情况适合使用float类型,例如,需要单精度数据的库,或者需要存储大量数据。

表示浮点型常量

表示单精度类型的常量需要在数字末尾加上一个后缀 F 或者 f。例如 3.141592F

表示双精度浮点型的常量需要在数字末尾加上一个后缀 D 或者 d 。例如 3.1415926

如果如果一个小数商量后面什么也不加,那么默认为double类型。

为什么double的有效位数只有15位?

// TODO 结合计数法等相关知识

参考 java中的float和double的精度问题

使用十六进制表示浮点数值

可以使用十六进制表示浮点数值。例如,$0.125=2^{-3}$ 可以表示成 0x1.0p-3。在十六进制表示法中,使用p表示指数,而不是e。注意,尾数采用十六进制,指数采用十进制。指数的基数是2,而不是10。

溢出和出错情况

  • 正无穷大 Double.POSITIVE_INFINITY。例如一个正整数除以零的结果为正无穷大。
  • 负无穷大 Double.NEGATIVE_INFINITY
  • NaN(不是一个数字) Double.NaN。例如计算 0/0 或者负数的平方根结果为 NaN

需要注意的是整数被0除将会产生一个异常,而浮点数被0除将会得到无穷大或者 NaN

检测一个变量是不是数字的方式

1
2
3
4
5
6
//正确方式,使用isNaN()方法
if (Double.iaNaN(x)){ ... };

//错误方式,直接和Double.Nan比较
if (x == Double.NaN){ ... };
//解释:所有“非数值”的值都被认为是不相同的,所以不能直接和Double.NaN比较

注意浮点数的误差

浮点数值不适用于无法接受舍入误差的金融计算中。例如,命令 System.out.println(2.0-1.1) 将打印出 0.8999999999999999 ,而不是人们想象中的 0.9。这种舍入误差的主要原因是浮点数值采用二进制系统表示,而在二进制系统中无法精确地表示分数 1/10。这就好像十进制无法精确地表示分数 1/3 一样。如果在数值计算中不允许有任何舍入误差,就应该使用 BigDecimal 类。

char类型

char类型常量的表示

必须使用单引号括起来的单个字符才是char类型常量。例如 'A'

如果使用双引号括起来单个字符,那么就是包含一个字符的字符串常量,不是char类型常量

转义字符

有一些字符因为已经被语言用来作为语法规则的一部分了,比如双引号 "。还有像换行符这种无法看到控制字符都可以采用转义字符来表示。

可是使用 \n 表示换行符,\t 表示制表符。

使用\"表示 "这个字符,这个时候 " 不再作为字符串的标志。

使用Unicode值表示字符

可以使用 \u0041 表示字符 A\u0008 表示控制字符 退格\u0022 表示字符 "

\u 后面跟的是4个十六进制的整数。

直接使用Unicode表示代码

比如,\u0053表示大写字母 S,那么直接用 \u0053 替换代码中的 S,程序仍然可以成功编译运行。例如:

1
2
3
4
5
public class Main {
public static void main(String[] args) {
\u0053ystem.out.println("Hello World");
}
}

使用Unicode表示法时候的注意事项

  • 注意点1

Unicode转义序列会在解析代码之前得到处理。例如"\u0022+\u0022" 并不是一个由引号(U+0022)包围加号构成的字符串。实际上,\u0022会在解析之前转换为 ",这会得到 ""+"",也就是一个空串。

例如

1
System.out.println("\u0022+\u0022") //输出空串

如果想要达到预期的结果需要这样:

1
System.out.println("\"+\"") //输出 "+"

或者这样:

1
System.out.println("\u005c\u0022+\u005c\u0022")
  • 注意点2:小心注释中使用Unicode的地方

比如:

1
//\u00A0 is a newline

这会产生一个语法错误,因为读程序时 \u00A0 会替换为一个换行符。

还有:

1
//Look inside c:\users

这也会产生一个语法错误,因为\u后面并未跟着4个十六进制整数。

Unicode和char类型拓展知识

//TODO Java核心技术 卷I 3.3.4

boolean类型

boolean(布尔)类型有两个值:falsetrue,用来判定逻辑条件。整形值和布尔值之间不能进行相互转换。

和C/C++不同的地方

在C/C++中,数值甚至指针可以代替boolean值,值0相当于布尔值false,非零相当于布尔值true。在Java中不是这样。

变量

变量名命名要求

  • 字符选择范围

与大多数程序设计语言相比,Java中“字母”和“数字”的范围更大。字母包括’A’~’Z’、’a’~’z’、’_’、’$’或者在某种语言中表示字母的任何Unicode字符。

比如,可以使用汉字作为变量名:

1
2
3
4
5
6
public class Main {
public static void main(String[] args) {
String 你好 = "你好世界";
System.out.println(你好);
}
}
  • 不能选择的字符

+©这样的符号不能出现在变量名中,空格也不能出现在变量名中。

  • 不建议选择的字符

尽管 $ 是一个合法的Java字符,但不要在你自己的代码中使用这个字符。它只用在Java编译器或其他工具生成的名字中。

  • 变量名区分大小写

比如,hireDayhireday 是两个不同的变量名。但是建议在对两个不同的变量进行命名的时候,最好不要只存在大小写上的差异。

变量初始化

声明一个变量之后必须对变量进行显式初始化,使用未初始化的变量会报错。

常量

在Java中,使用关键字 final 指示常量。关键词 final 表示这个变量只能被赋值一次,一旦被赋值之后就不能再更改了。习惯上,常量名使用全大写。

示例:

1
final double CM_PER_INCH = 2.54;

类常量

Java中可以使用关键字 static final 定义类常量。

类常量只能定义在一个类中所有的方法之外,不能定义在类的某个方法当中。

如果定义类常量时同时把变量声明为一个 public 变量,那么其他类的方法也可以使用这个常量。

示例:

1
2
3
4
5
6
7
8
9
public class Constants2
{
public static final double CM_PER_INCH = 2.54;

public static void main(String[] args)
{
//...
}
}

这样,别的类就可以通过 Constants2.CM_PER_INCH 使用这个类常量。

另外,关键字 const 在Java中是保留字,但目前并没有使用。

运算符

关于浮点计算

可移植性是Java语言的设计目标之一。无论在哪个虚拟机上运行,同一运算应该得到相同的结果。对于浮点数的算术运算,实现这样的可移植性是相当困难的。double类型使用64位存出一个数值,而有些处理器使用80位浮点寄存器。这些寄存器增加了中间过程的计算精度。

这就对在不同机器上产生相同的计算结果带来的困难。解决方法是使用 strictfp 关键字来对方法或者类进行标记。

//TODO 这里有待进一步解释 参考Java核心技术 卷I 3.5

数学函数与常量

在Math类中,包含了各种各样的数学函数。

  • 求平方根
1
Math.sqrt() //计算一个数的平方根
  • 求幂
1
double y = Math.pow(x, a); //将y的值设置为x的a次幂;此方法的两个参数都是double类型,返回结果也是double类型
  • π和e的近似值

Math.PIMath.E

关于结果的 完全可预测性计算速度 之间的取舍

在Math类中,为了达到最快的性能,所有的方法都使用计算机浮点单元中的例程。如果得到一个完全可预测的结果比运行速度更重要的话,那么就应该使用 Strictmath类。它使用“自由发布的Math库”(fdlibm)实现算法,以确保在所有平台上得到相同的结果,有关这些算法的源代码参看www.netlib.org/fdlibm。

数值类型之间的转换

精度较高的类型转换为精度较低的类型时会丢失精度。

不同数值类型相互运算

两个数值类型的变量之间的运算根据以下规则进行:

  • 如果两个操作数中有一个是double类型,另一个操作数就会转换为double类型。
  • 如果两个操作数中有一个是float类型,另一个操作数就会转换为float类型。
  • 如果两个操作数中有一个是long类型,另一个操作数就会转换为long类型。
  • 否则,两个操作数都将被转换为int类型。

强制类型转换

根据上一个小节可以知道,必要的时候,int类型的值会被自动转换为double类型。

有时候也需要把double转换为int,这种转换通过 强制类型转换 实现。

示例:

1
2
double x = 9.997;
int nx = (int)x;

这样,nx 的值就变为9,强制类型转换通过截断小数部分将浮点类型转换为整形。

  • 通过 Math.round() 进行舍入运算

示例:

1
2
double x = 9.997;
int nx = (int) Math.round(x);

这种方式先使用 Math.round() 对变量 x 进行了舍入运算,但是返回值为 long,所以还需要再进行强制类型转换为 int型。

Math.round() 的参数为 double 时,返回值为 long 类型;参数为 float 时,返回值为 int 类型。

  • 强制类型转换可能出错的情况

如果试图将一个数值从一种类型转换为另一种类型,而又超出了目标类型的表示范围,结果就会截断成一个完全不同的值。例如 (byte) 300 的实际值为44。

  • 不要对 boolean 类型进行强制类型转换

不要在boolean类型与任何数值类型之间进行强制类型转换,这样可以防止发生错误。只有极少数的情况才需要将布尔类型转换为数值类型,这时可以使用条件表达式:

1
int a = b ? 1 : 0;

自动进行强制类型转换的情况

如果运算符得到一个值,其类型与左侧操作数的类型不同,就会发生强制类型转换。例如,如果x是一个int,则以下语句

x += 3.5;

是合法的,将把x设置为(int)(x+3.5)

三元运算符 condition ? expression1 : expression2

在合适的地方使用 condition ? expression1 : expression2 能让代码简洁清晰。

位运算符(逻辑类型)

位运算符是针对二进制的位进行操作的。

与(and):& - 只有两个位都为1的时候,运算结果才为1,否则为0

或(or):| - 两个位中有1个为1,运算结果就为1,否则为0

异或(xor):^ - 两个位相同则结果为0,不同则为1。

非(not):~ - 按位取非

  • 可以通过掩码技术得到整数中的各个位。
  • 应用在布尔类型上

&| 作用在布尔类型上时,也会得到一个布尔值。这个结果和使用 &&|| 类似。但是区别在于,&&|| 采用“短路”求值的方式,比如若 && 的第一个操作数为false,那么就不会对第二个操作数进行计算了。但是 & 会对两个操作数都进行计算。

位运算符(移位运算)

左移位:<<

右移位:>>

特殊的右移位:>>>

移位运算符的左操作数是被移的数,右操作数是要移动的位数。

num << 1 相当于左移一位,相当于num乘以2。即整体左移移位,低位补0。

num >> 1 相当于右移一位,相当于num除以2。即整体右移一位,符号位不动,和符号位相邻的位补上和符号位一样的数。

num >>> 1 相当于右移一位,和 >> 不同的是,这个运算符会让符号位也一起向右移动。高位补0。

注意:没有 <<< 这个运算符。

注意

移位运算符本身不是原地运算,也就是说,不能写出一条单独的语句 a >> 1;,要写成 a = a >> 1;

或者使用 >>= 或者 >>>= 或者 <<=。也就是写成 a >>= 1; 这样。

枚举类型

枚举类型的变量只能存储这个类型声明中给定的某个枚举值。

示例:

1
2
enum Size { SMALL, MEDIUM, LARGE, EXTRA_LARGE};
Size s = Size.MEDIUM;

循环语法

for 循环

for ( [ForInit] ; [Expression] ; [ForUpdate] ) Statement

for语句的第一句是初始化语句,但是这个初始化语句只能放一个初始化语句,这就意味着初始化语句只能定义一种类型的变量。比如 for(int a = 0, b = 1;;) 这样是正确的。但是 for(int a = 0, double b = 2.0;;) 就是错误的。

关于 for 语句的初始化 14.14.1.1. Initialization of for Statement

那么如果想要在 for 语句中初始化两个不同类型的变量怎么办呢?

可以考虑把 for 语句放在一个代码块中,也就是这样:

1
2
3
4
5
6
7
8
{
int a = 0, b = 1;
double c = 3.0;
for (; a < 10 && b < 10; ++a, ++b) {
System.out.println("a:" + a);
System.out.println("b:" + b);
}
}

这样,变量 a 、变量 b 和变量 c 的作用范围都只在代码块中。