Java学习笔记(一)——Fundamental Programming Structures in Java

本博客总结自《Java核心技术卷一》中的第三章的内容。

1.数据类型

Java是一个强类型的语言。共有八种基本类型:byte(1)、short(2)、int(4)、long(8)(四种整型)、double(4)、float(8)(两种浮点类型)、char(字符型)、boolean。
(注:big bumbers是一种java对象,并不是一种java类型)

(1)整型
java中,整型的大小是与平台无关,这与C和C++有所不同。而且java中的整型并不包括无符号(unsigned)的整型。
(注:C与C++中整型的范围基于目标平台,在16bit的处理器中(8086),int为2bytes,long为4bytes。在32bit的处理器中,int为4bytes,long为8bytes。)

长整型通常包含一个后缀L或l(40000000000L)。

在Java SE7中,可以使用0b开头的写法,表示二进制数。同样还可以使用下划线_,1_000_000表示一百万的大小,这只是为了方便程序员能够更好的区分数值大小,编译器会自动忽略它。

(2)浮点类型
float类型通常都有一个后缀F或f表示。如果不添加后缀。比如3.14将会被自动识别为double类型(当然你也可以选择性的添加D或d作为后缀)。

java中的所有浮点类型都遵循IEEE 754的标准。特别的,有三种特殊的浮点类型分别指代溢出和错误:
1.正无穷大(+/0);————Double.POSITIVE_INFINITY
2.负无穷大(-/0);————Double.NEGATIVE_INFINITY
3.NaN;(0/0)————Double.NaN

特别需要指出的是,尽管常量Double.NaN指代的是非数值,但是并不能将其哪来作为比较。x == Double.NaN将会永远返回false。因为任何NaN类型都不与其自身相等(这与js还是十分一致的)。

但是我们可以使用提供的方法Double.isNaN(x)来判断x是否是一个非数值。

(注:java中的浮点类型并不适用于对于精度要求非常高的金融计算领域中,因为舍入(roundoff)是无法容忍的。例如2.0-1.1将会返回0.8999999999999999而不是0.9,这是因为浮点类型是用二进制表示的,而二进制是无法精确表示1/10的,正如在十进制中是无法精确表示1/3的。如果想要避免roundoff的出现,建议使用BigDecimal类)

(3)字符型(char)
字符型最初始时是为了表示单个的字符,但是现如今已经不再是这样的。例如某些Unicode字符能够使用1个char来表示,但是某些Unicode字符确需要两个char来表示。

char同样能够使用十六进制个表示(\u0000~\uFFFF)。

常见的转义字符:\b(空格,\u0008),\t(制表符,\u0009)、\n(换行,\u000a)、\r(回车,\u000d)、\”(双引号)、\’(单引号)、\(反斜线)。

java使用的16bit的unicode字符集。

之后不可避免的事情发生了,unicode字符集扩展超出了65535的范围。从Java SE 5.0开始,码点(code point)是一个关联到单个字符编码模式的码值(code value)。在unicode的标准中,码点由U+开头的十六进制数表示,例如U+0041表示拉丁文中的字母A。同时码点被分为17组多语言平面(code planes),基本多语言平面则表示码点范围(U+0000~U+FFFF)。另外十六个“平面”则表示范围(U+10000~U+10FFFF),作为补充的字符集。

UTF-16编码表示所有的unicode编码的码点都是变长的。单个字符在基本多语言平面中用16bit表示,被称作码元(code unit)。而java中的char则描述的是单个码元在UTF-16的编码模式中。

书中有一个强烈的建议是不要使用char类型在编码中(除非你需要操作UTF-16的码元),否则最好使用String类。

(4)布尔类型(boolean)
在java中,你不能在整型和布尔型之间进行转换。而C++中不同,0可以等价于false,非零值可以等价于true。

2.变量和常量

java中,如果变量声明但是未初始化而去使用它,则会报错。同时一个好的习惯是,将变量声明在初次使用它的地方。

在java中,你可以通过使用final关键字声明一个常量。例如: final double CM_PR_INCH = 2.54;

通常你可能需要在某个类的多个方法中使用某个常量,这往往被称为类常量(class constants)。声明一个类常量可以通过关键字static final。例如在某个类中:static final double CM_PR_INCH = 2.54; 。需要注意的是类常量不能声明在main方法中。

当然如果你希望该常量能够在其他类中被使用,则可以加上public关键字:public static final double CM_PR_INCH = 2.54; 。

(C++和js(ES6)中使用const声明常量,const在java中仅仅是保留字)

3.枚举类型

通常,某个变量可能需要持有若干个受限制的值的集合。这就是所谓的枚举类型。枚举类型是有限的(被命名的)数值集合。例如: enum Size { SMALL, MEDIUM, LARGE, EXTRA_LARGE };(or null)

这个时候你就可以声明某个变量为该枚举类型:Size s = Size.MEDIUM;

4.Strings字符串

概念上的,Java strings是一串Unicode字符。Java中并没有内建(built-in)string类型。取而代之的是在标准的Java库中包含有一个预定义的类String。所有的string都是String类的实例。

常见的String类中的静态方法(除了(2)):
(1) String greeting = “Hello”; String s = greeting.substring(0, 3);
(2)Concatenation: + ;
当+号的操作的两种类型,一种是String,另一种是其他类型时,会发生强制类型转换(to string),甚至包括对象。
(3)join: String all = String.join(“/”, “S”, “M”, “L”, “XL”); 将会得到”S/M/L/XL”

string都是不可变的,JAVA中并没有提供一种方法让你去改变某个字符串中的某个字符,因为字符串实例都是不可变的。那么如何操作字符串实例得到想要的字符串呢?非常简单,采用substring和+的方式(substring and concatnation)。(或者改变引用,将当前字符串变量引用到另一个字符串实例就好了)

那么为何要这样呢?明明改变某个字符串中字符的码元(code-unit)来得到想要的字符串开销是更低的。但是保持字符串实例的不可变性有一个唯一的优点,编译器能够将这些strings实例变为可分享的(shared)。应用中的多个strings实例都是保存在一个公共的池子(common pool)(heap,堆内存)中。如果你复制某个字符串变量所引用的实例,那么这个复制的字符串是可共享的(多(变量)对一(实例)的引用)。这个共享字符串实例的优点相比于减少操作字符串的额外的开销,往往从程序的角度会更优。因为,在应用中相对来说会更少的改变strings,通常而言更多的是比较这些strings实例。

判断两个字符串实例是否equal(外形相同):s.equals(t); (s和t可以是字符串变量或字符串实例)。
忽略大小写的字符串比较: s.equalsIgnoreCase(t);
不要采用==去判断两个字符串是否相等,这样仅仅是比较两个字符串实例是否存放在相同的位置(相同的引用)。

前面提到过字符串实例的共享机制,确实这样看来,用==去比较共享实例的字符串是没有任何问题的(因为如果要比较的字符串变量引用的都是同一个位置的字符串实例,那么自然是相等的(equal),但实际上,这仅限于创建或复制的字符串实例,对于substring或+操作字符串后得到的新字符串实例可能就不适用了,比如”Hello”.substring(0, 3) == “Hel”很有可能就会返回false)。

空字符串””和null:
(1)””:持有length为0,content为空的JAVA对象。(判断一个字符串变量是否为空字符串: str.equals(“”)或者是str.length == 0;)
(2)null:若某个变量被赋值null,则表示没有任何对象的引用与当前变量关联。(str == null)

重点,Building Strings

某种情况下,可能需要通过许多更短的strings创建(拼接)成更长的strings。如果直接采用+的方式,不仅会浪费时间,而且会浪费内存。因为在这个过程中,每当你concatenate一次strings,就会创建一个新的strings。而使用StringBuilder类可以避免这种问题。具体过程如下:
(1)创建一个空的string builder: StringBuilder builder = new StringBuilder();
(2)每当你想要”加上”某一个部分,就调用append方法: builder.append(ch);(加上一个char类型的字符) builder.append(str);(加上一个String类型的字符串)
(3)确定加完之后,调用toString方法,这样你就会得到一个构建好的字符串实例:String completeString = builder.toString();

StringBuilder类是在JDK5.0提出的,它的前辈是StringBuffer。虽然相比之下,效率更低一点,但是它允许多线程操作(添加或移除)字符。当然,如果所有的字符操作都在单一的线程中(通常情况),就应该使用StringBuilder。

最后需要补充的就是StringBuilder还有insert和delete等方法,支持插入和删除的操作。

5.输入和输出

标准输出流:System.out.println();
标准输入流:System.in;

如果想要读取用户的输入,首先需要借助工具类Scanner,并在构造器中传入System.in作为参数。Scanner in = new Scanner(System.in);

然后就可以通过Scanner实例in来读取输入,Scanner类中提供了许多实例化的方法,用于读取输入。in.nextLine()用于读取下一行的输入,in.next()用于读取下一个“单词”的输入(以空格为界),in.nextInt()用于读取下一个整型输入。

由于Scanner类是java.util工具包中的工具类,所以在使用前需要导入该包:import java.util.* 。(除了java.lang包中的类不需要导入,可以在代码中直接使用外,其他包中的类都需要先导入,再使用)

例子如下:

import java.util.*;

public class InputTest {
	public static void main(String[] args) {
		Scanner in = new Scanner(System.in);
		
		System.out.println("your name?");
		String name = in.nextLine();
		
		System.out.println("how old r y?");
		Int age = in.nextInt();
		
		System.out.println("Hello: " + name + "Next year you will be: " + (age + 1));
	}
}

Scanner类并不适用于在控制台读取密码输入,因为所有的输入都是明文显示的。Java SE6引入了Console类来完成此项需求。例如:

Console cons = System.console();
String uesername = cons.readLine("User name: ");
char[] passwd = cons.readPassword("Password: ");

处于安全性的考量,返回的password将是字符类型的数组,而不是一个字符串。

Console类并没有Scanner类那么方便,因为必须每次读取一整行。

格式化输出:在早期的Java版本中,格式化数值的输出一直都是比较麻烦的事情,知道Java SE5引入了C库函数中的printf。例如:System.out.printf("%8.2f", x);将会以小数精度为2,字符长度为8的形式输出数值x。同时你也可以输入多个参数到printf函数中,例如:System.out.printf("Hello, %s.Next year you will be %d", name, age);

每一个格式化说明符(format specifiers)都以%开头,说明该字符将会以关联的参数替代。还有格式化标记(flags),用于标记输出,例如:System.out.printf("%,.2f", 10000.0 / 3.0);将会得到3,333.33。更多的格式化说明符可以阅读书中82,83,84页中的表格。

当然以上都是输出时的格式化,那么在程序中如何进行字符串的格式化呢?String类中提供了format静态方法,用于格式化字符串:String message = String.format("Hello, %s. Next year you will be: %d", name, age);可以看到实际上的用法与前面输出格式化printf()是一致的。

为了趋于完整性描述printf功能的意愿,实际上printf还提供了格式化输出时间字符串的功能。(当然在一些老旧的代码中可能会使用到(legacy code),新代码中建议使用java.time包中的类来格式化输出时间)

例如: System.out.printf("tc%", new Date());其中格式化说明符以t开头,其他字符结尾(该字符决定了具体如何格式化输出时间)。此处用的字符c将会格式化输出时间如下: Mon Feb 09 18:05:19 PST 2015。(完整的输出日期和时间)。具体还可以使用哪些参数,可以查看85,86页的表格。不过,逐个去格式化时间(日,月,年…)是比较麻烦的,所以printf还允许添加索引分别格式化的方式输出时间:System.out.printf("%1$s %2$tB %2$te, %2$tY", "Due date", new Date());将会打印:Due date: February 9, 2015。(索引以%开头,$结尾,索引从1开始,而不是0)
(注:格式化输出的日期是与特定地区相关的,不同地区,相同的格式化方式输出显示的时间可能有所不同)

文件输入及输出:

读取一个文件,同样需要用到Scanner工具类:Scanner in = new Scanner(Paths.get("myFile.txt"), "UTF-8");如果文件路径中有斜线,记得转义:Scanner in = new Scanner(Paths.get("C:\\myFile.txt"), "UTF-8");。通常情况下,输出的编码格式采用UTF-8,这也是因特网中大多数文件字符的格式。如果不填写编码的选项,默认将会采用JAVA程序运行的计算机所采用的字符编码方式。这当然是不妥当的,基于JAVA的跨平台性,这可能导致不同平台上的不同编码方式。

写入一个文件,需要用到PrintWrite类,例如:PrintWrite out = new PrintWrite("myfile.txt", "UTF-8");。如果该文件不存在,它将默认创建该文件。

(注:如果在读取或者写入文件的路径采用的是相对路径,那么该相对路径相对的路径是当前JAVA虚拟机开始运行的文件位置,如果采用命令行的方式如java MyProg启动程序,则相对路径是当前命令行shell程序所在的目录。如果采用的是集成开发环境,那么它控制着相对路径的位置,可以使用String dir = System.getProperty("user.dir")的方式获取当前的文件相对路径)

最后需要注意的一个问题就是,如果你在读取(Scanner)一个不存在的文件或写入(PrintWrite)一个无法写入对应格式的文件时,JAVA编译器将会抛出一个异常,而且这个异常远比除0异常严重。之后将会详细介绍处理异常的方法,现在可以简单的告知编译器你会注意输入输出时可能造成的异常,通过以下方式:

public static void main(String[] args) throws IOException {
	Scanner in = new Scanner(Paths.get("myfile.txt", "UTF-8"));
	//......
}

现在我们仅仅是介绍了如何简单的读取文本文件的数据,对于更高级的用法,比如如何处理不同字符的编码模式,处理二进制数据,读取目录以及写入压缩文件,都将在第二卷的第二章中介绍。

当你通过命令行的方式启动程序时,可以在shell中使用重定向语法(redirection syntax),指定输入输出的文件路径:java MyPro < myfile.txt > output.txt。这样你就不用担心处理IO异常。

7.控制流

块级作用域:块({})定义了变量的作用范围。块可以相互嵌套。嵌套的块中是不允许定义相同变量名的:

public static void main(String[] args) {
	int n;
	
	{
		int n;		//error:cannot redefine n in inner block	
	}
}

(不过,这在C++中是允许的,内层的变量将会覆盖(遮蔽,shadows)外层的同名变量:))

接下来就是千篇一律的if,elseif,else,while,do-while,for,switch环节,这里就不一一介绍了。

其中在for循环中需要注意累增值为小数的情况,这有可能导致循环无限执行下去,同样是由于roundoff错误导致的。例如:

for (double x = 0; x != 10; x += 0.1) {
	//may never end...
}

还有一个地方需要注意的就是,与JavaScript不同,在循环中定义的初始变量仅在循环内部有效,循环外是无效的(未定义的)。所以,如果你还想继续使用循环结束后已经变化的初始变量值,一定要记得在循环外声明它。

int i;
for (i = 1; i <= 10; i++) {

}
//i is still defined here.

此外,书中还不建议使用switch,因为一旦忘记break,将可能导致贯穿执行,导致程序异常。可以使用命令行javac -Xlint:fallthrough Test.java来让编译器在发现程序中出现fallthrough的情况就立刻警告。当然如果你确实想要使用fallthrough的特性,可以在对应的方法上添加注释(annotation)让编译器忽略它:@SuppressWarnings("fallthrough")

case后面可以跟以下类型:
(1)直接量类型:char,byte,short 或者是int;
(2)枚举类型;
(3)从JAVA SE7开始,可以使用字符串字面量(string literal)。

如果是枚举类型的switch,可以省略枚举变量实例(因为在switch中已经指定),例如:

Size sz = ...;

switch(sz) {
	case SMALL:
		...
		break;
	...
}

break与标记break(labeled break):可以给任何声明添加标记label,用于程序跳出循环break时使用,跳到指定的位置并执行(但是并不建议使用)。
continue:中止执行当前循环的余下代码,并进入下一次循环中。

8.大数(Big Numbers)

当基本类型整型和浮点型不能处理某些情况的时候,可以采用java.math包中提供的极其方便的两个工具类:BigInteger和BigDecimal。这两个类可以用来操作任意长度的数值。
BigInteger类实现了任意精度的整数算法,BigDecimal则是相同的浮点数版本。使用也非常的简单:

使用静态方法valueOf()将一个普通的数值变为Big Number类型:BigInteger a = BigInteger.valueOf(100);。不过对于大整型,不能使用+或* 对其进行运算。对应的可以使用add和multiply等实例方法(还有subtract,divide,mod)。

BigInteger c = a.add(b);
BigInteger d = c.multiply(b.add(BigInteger.valueOf(2)))

Java并不像C++那样提供了操作符覆盖(overloading)的功能。并没有方法针对特定的BigInteger类重新定义操作符+和* 的行为。(java语言的设计者只在Stirng类中重定义了+的行为,concatnate,但是其他操作符都没有被重定义,而且也不允许开发者在自定义的类中重定义操作符的行为)

9.数组

在强类型语言中,数组是存放某种特定类型元素的集合。同时允许通过索引的方式得到数组中的值。(例如JavaScript这种弱类型语言,声明的数组可以混合存放任何类型的元素,甚至是empty:))

声明一个数组可以采用类型+[]的方式,例如:int[] a;或者是int a[];

声明的同时还可以初始化一个数组:int[] a = new int[100]; (这样就初始化了一个可以存放100个int类型元素的数组,而且所有位置的初始元素为0——如果是布尔类型的数组,则会默认填充false;对象(例如String[])数组则会默认填充null)。

获取某个数组的长度可以使用length属性。

需要注意的是,一旦创建了一个数组,那么它的长度将无法被改变。如果需要在代码运行中动态的扩展数组的长度,应当使用不同的数据结构arraylist。

for each循环:Java中提供了增强的foreach循环,语法如下: for(variable : collection) statement。(for each variable in collection, collection可以是数组也可以是实现了Iterable接口的类的实例对象)

tip:打印字符串形式的数组十分简单,可以直接使用Array类的实例方法toString()。例如:Array.toString(arr);将会得到类似于”[1, 2, 3, 4]”的字符串。

数组的初始化和匿名数组:
(1)声明的同时初始化数组:int[] p = { 2, 3, 4, 5, 6 };
(2)匿名数组:new int[] { 1, 2, 3, 4, 5 };

声明一个包含零个元素的数组是合法的:int[] arr = new int[0];值得注意的是,不包含元素的数组并不等价于null。

数组复制:如果通过赋值的方式在变量间复制数组,仅仅是复制的引用,如果想要真正意义上的复制所有的数组元素。则可以使用Array.copyOf()方法,还有copyOfRange(type[] a, int start, int end)。例如:int[] copiedArray = Array.copyOf(arr, newArrLength);。第二个参数是复制后的新数组的长度。(如果大于被复制数组的长度,则如果是int型数组,以0填充;boolean则以false填充)

我们一定注意到main方法中接收一个String类型数组作为参数(String[] args),这个字符串数组实际上接收的是命令行的输入,例如:

public class Message {
	public static void main(String[] args) {
		if (args.length == 0 || args[0].equals("-h")) {
			System.out.println("Hello");
		} else if (args[0].equals("-g")) {
			System.out.println("GoodBye");
		}
		for (int i = 1; i< args.length; i++) {
			System.out.print(" " + args[i]);
		}
		System.out.println("!");
	}
}

然后运行如下命令: java Message -g cruel world 将会得到 GoodBye, cruel world! 其中args[0]就是-g,以此类推。

数组排序:Array.sort();
例如: int[] a = new int[1000]; Array.sort(a);sort方法使用调整过的快排算法进行排序,所以一般效率非常高。

发表评论

电子邮件地址不会被公开。 必填项已用*标注