Java基础笔记(一) 数据类型、面向对象、基础类库

本文主要是我在看《疯狂Java讲义》时的读书笔记,阅读的比较仓促,就用 markdown 写了个概要。

第一章 Java概述

Java SE:(Java Platform, Standard Edition)整个Java技术的核心和基础,它是Java ME和Java EE编程的基础。
Java ME:(Java Platform, Micro Edition)主要用于控制移动设备和信息家电等有限存储的设备。
Java EE:(Java Platform,Enterprise Edition)提供了企业应用开发相关的完整解决方案,是Java技术中应用最广泛的部分。


JVM:(Java Virtual Machine)Java虚拟机,负责解释执行字节码文件。(JVM是Java程序跨平台的关键)


1、Java程序的组织形式

Java是一种纯粹的面向对象的程序设计语言,即必须以类(class)的形式存在。类是Java程序的最小程序单位。(Java程序不允许可执行语句、方法等成分独立存在,所有的程序部分都必须放在类定义里)

Java程序的入口是一个类中的main方法public static void main(String[] args)

2、源文件的命名规则

3、垃圾回收机制

Java不需要程序员直接控制内存回收,程序的内存分配和回收都是由JRE在后台自动进行的。JRE会负责回收那些不再使用的内存,这种机制称为垃圾回收(Garbage Collection)


第二章 Java数据类型

Java中的所有关键字(都是小写):

除了上面48个关键字之外,Java还包含gotoconst两个保留字(未来可能用作关键字)与 三个特殊的字面值:truefalsenull


引用类型就是对一个对象的引用。实际上,引用类型变量就是一个指针,只是Java语言里不再使用指针这个说法。


第三章 深入Java数组

1、数组的定义与初始化

在Java中,数组也是一种数据类型,而且是一种引用数据类型。

定义数组

1
2
type[] arrayName;  // 建议使用这种形式
type arrayName[];

注意:定义一个数组时,仅仅是定义了一个引用变量(也就是一个指针),它还未指向任何有效的内存,因此,定义数组时不能指定数组的长度。


2、数组在内存中的运行机制

数组是一种引用数据类型,所以数组变量只是一个引用。通过这个引用访问它所指向的有效内存(数组对象本身)。

通常,如果数组引用变量是一个局部变量,它会被存储在栈(stack)内存中,而实际的数组对象被存储在堆(heap)内存中,如下图所示:

下面看一个例子:

1
2
3
int[] a = {1,2,3};
int[] b = new int[4];
b = a;

从上面的例子可以看出,Java的引用类型就相当于C/C++中的指针类型。


3、栈内存与堆内存

栈内存:当一个方法执行时,每个方法都会建立自己的内存栈,在这个方法内定义的变量将会逐个放入这块栈内存里,随着方法的执行结束,内存栈也将自然销毁。

堆内存:当我们在程序中创建(new)一个对象时,该对象会被保存到运行时数据区中,以便反复利用(因为对象的创建成本通常较大),这个运行时数据区就是堆内存。堆内存中的对象不会随方法的结束而销毁,只有当一个对象没有任何引用变量引用它时,系统的垃圾回收器才会在合适的时候回收它。

操作数组的工具类:java.util.Arrays


第四章 面向对象(上)

定义类:

1
2
3
4
5
6
[修饰符] class 类名
{

零到多个构造器定义
零到多个Field
零到多个方法
}

其中的修饰符可以是publicfinalabstract或省略。


  1. 类也是引用数据类型,用类定义的变量也只是一个引用(或者说指针),里面只是存放了一个地址值。

  2. static修饰的方法不能直接访问没有static修饰的成员。(理由很简单:静态成员是独立于具体对象而存在,属于类本身,而非静态成员是依赖于具体的对象的)

  3. Java里方法的参数传递方式只有一种:值传递。基本数据类型和引用数据类型都是将实参的一个副本传给形参,只不过引用数据类型拷贝的是地址值!


可变参数函数:

JDK 1.5之后,Java允许为方法指定数量不确定的形参,通过在最后一个形参的类型后增加三个点(…),代码实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyClass {
/**
* 可变参数的方法
*/

public static void func(int a, String... str) {
for(String s : str) {
System.out.println(s);
}
System.out.println(a);
}

/**
* 程序入口-main
*/

public static void main(String[] args) {
func(15, "第一个字符串","第二个字符串","第三个字符串");
}
}

当然,如果你觉得这样麻烦,可以直接用一个数组代替


方法重载:

Java允许同一个类里定义多个同名方法,只要形参列表不同就行。


在Java中,根据变量定义位置的不同,可以将变量分为两大类:成员变量 和 局部变量

1、成员变量的初始化和内存中的运行机制

  • 当系统加载类、或者创建该类的实例时,系统自动为成员变量分配内存空间,并自动指定初始值。
  • 类 Field(静态成员变量),系统会在类加载时为其分配内存空间,并指定默认初始值。
  • 实例 Field 是在创建实例时分配内存空间并指定初始值的,注意:实例变量指向的是这部分内存。

2、局部变量的初始化和内存中的运行机制

  • 局部变量定义后,必须经过显式初始化后才能使用,系统不会为局部变量执行初始化。
  • 也就是说,定义局部变量后,系统并未为这个变量分配内存空间,直到等到程序为这个变量赋初值时才分配,并将初值保存在这块内存中。
  • 局部变量总是保存在其所在方法的栈内存中,所以它不属于任何类或实例。
  • 如果是基本类型的局部变量,则栈内存中是变量的值;如果是引用类型的局部变量,则栈内存中存放的是地址,引用堆内存中的实际对象。

类的封装

封装是面向对象的三大特征之一。为了实现良好的封装,需要:

  • 将 Field 和实现细节隐藏起来,不允许外部直接访问。
  • 把方法作为外部接口暴露出来,让方法来控制对 Field 进行安全的访问和操作。

Java中提供了4个访问控制级别:privateprotectedpublic和不加任何访问控制符(default),它们的访问控制级别由小到大:


package、import 和 import static

包(package):为了解决类的命名冲突,Java引入了包机制,提供了类的多层命名空间。

如果一个类被放于某个包中,则我们应该在该Java源文件的第一个非注释行添加如下代码:

1
package packageName;

import语句可以导入指定包下某个类或全部类,但import语句并不是必需的,只要坚持在类里面使用其他类的全名,则可以无须使用import语句。

注意:在JDK 1.5以后增加了一种静态导入(import static)的语法,用于导入指定类的某个静态 Field、方法或该类全部的静态 Field、方法。

1
2
3
import static package.subpackage...className.fieldName;  // 导入某一静态变量
import static package.subpackage...className.methodName; // 导入某一静态方法
import static package.subpackage...className.*; // 导入该类的所有静态Field、方法

用一句话归纳importimport static的作用:使用import可以省略写包名,而使用import static则可以连类名都省略。


Java的常用包

Java的核心类都放在java这个包及其子包下,Java扩展的许多类都放在javax包及其子包下。下面几个包是Java语言中的常用包:

  • java.lang:这个包下包含了Java语言的核心类,如 String、Math、System 和 Thread 类等,使用这个包下的类无须使用 import 语句导入,系统会自动导入这个包下的所有类。
  • java.util:这个包下包含了Java的大量工具类/接口和集合框架类/接口,例如 Arrays、List 和 Set 等。
  • java.net:这个包下包含了一些Java网络编程相关的类/接口。
  • java.io:这个包下包含了一些Java输入/输出编程相关的类/接口。
  • java.text:这个包下包含了一些Java格式化相关的类。
  • java.sql:这个包下包含了Java进行 JDBC 数据库编程的相关类/接口。
  • java.awt:这个包下包含了抽象窗口工具集(Abstract Window Toolkits)的相关类/接口,这些类主要用于构建 GUI 程序。
  • java.swing:这个包下包含了 Swing 图形用户界面编程的相关类/接口,这些类可用于构建平台无关的 GUI 程序。

构造器

构造器也就是构造函数!!!

Java类可以包含一个或一个以上的构造器。一旦程序员提供了自定义的构造器,系统就不再提供默认的无参构造器了。(所以如果为一个类编写了有参数的构造器,通常建议为该类也额外提供一个无参数的构造器)


类的继承

继承是面向对象的三大特征之一。Java的继承具有单继承的特点,每个子类只有一个直接父类。

继承的语法格式:

1
2
3
4
修饰符 class Derive extends Base
{

// 类定义部分
}

Java使用extends作为继承的关键字,extends在英文中是扩展的意思。

重写父类的方法要遵循“两同两小一大”的规则:

  • “两同”:方法名相同,形参列表相同。
  • “两小”:子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等。
  • “一大”:子类方法的访问权限应比父类方法的访问权限更大或相等。

如果需要在子类方法中调用父类中被覆盖的方法,若被覆盖的是实例方法,使用super作为调用者;若被覆盖的是类方法,使用父类类名作为调用者。


构造器的执行顺序

子类不会获得父类的构造器,但子类构造器里可以调用父类构造器。有如下几种情况:

  • 子类构造器函数体的第一行使用super显式调用父类构造器。
  • 子类构造器函数体的第一行使用this显示调用本类中重载的构造器,执行本类中另一个构造器时即会调用父类构造器。
  • 子类构造器函数体中既没有super调用,也没有this调用,系统将会在执行子类构造器之前,隐式调用父类无参数的构造器。

不管上面哪种情况,当调用子类构造器来初始化子类对象时,父类构造器总会在子类构造器之前执行;不仅如此,执行父类构造器时,系统会再次上溯执行其父类构造器……依此类推,创建任何Java对象,最先执行的总是java.lang.Object类的构造器。


多态

多态是面向对象的三大特征之一。

Java引用变量有两个类型:一个是编译时类型,一个是运行时类型。编译时类型由声明该变量时的类型决定,运行时类型由实际赋给该变量的对象决定。两个类型不一致时,就可能出现多态。

当把一个子类对象直接赋给父类引用变量时,这个引用变量的编译时类型是 BaseClass,而运行时类型是 SubClass,当运行时调用该引用变量的方法时,其方法行为总是表现出子类方法的行为特征,而不是父类方法的行为特征。也就是说:相同类型的变量 调用同一个方法时,呈现出多种不同的行为特征,这就是多态。

多态的两个前提: 要有继承(inheritance),要有方法重写(override)


instanceof 运算符

instanceof是Java中的一个二元运算符,它的作用是在运行时判断左边对象是否是右边类(或其子类)的实例。如果是,返回true,否则返回false

1
2
3
4
5
6
7
8
9
10
11
12
public class MyClass {

public static void main(String[] args) {
String str = ""; // str 是String类型引用变量
Object obj = ""; // obj 的编译时类型是 Object,但实际类型是 String
System.out.println("str 是 String 的实例:" + (str instanceof String)); // true
System.out.println("str 是 Object 的实例:" + (str instanceof Object)); // true
System.out.println("obj 是 String 的实例:" + (obj instanceof String)); // true
System.out.println("obj 是 Object 的实例:" + (obj instanceof Object)); // true
System.out.println("obj 是 Math 的实例:" + (obj instanceof Math)); // false
}
}

需要注意:instanceof运算符前面操作数的编译时类型要么与后面的类相同,要么与后面的类具有父子继承关系,否则会引起编译错误。

instanceof运算符的常用之处:在进行强制类型转换之前,首先判断前一个对象是否是后一个类的实例,是否可以成功转换,从而保证代码更加健壮。


初始化块

初始化块是Java类里可出现的第4种成员(前面依次有 Field、方法和构造器)。与构造器的作用类似,初始化块也可以对Java对象进行初始化操作。

初始化块的语法格式如下:

1
2
3
4
[修饰符] {
// 可执行代码
...
}

初始化块的修饰符只能是static,使用 static 修饰的初始化块被称为静态初始化块

一个类里可以有多个初始化块,先定义的初始化块先执行,后定义的初始化块后执行,下面是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MyClass {

{
int a = 5;
System.out.println("第一个初始化块!");
}

{
System.out.println("第二个初始化块!");
}

public MyClass() {
System.out.println("类的无参数构造器!");
}

public static void main(String[] args) {
new MyClass(); // 创建一个对象
}
}

控制台输出结果:

1
2
3
第一个初始化块!
第二个初始化块!
类的无参数构造器!

可以看出,初始化块是在构造器之前执行的。创建一个 Java 对象时,不仅会执行该类的普通初始化块和构造器,而且系统会一直上溯到java.lang.Object类,先执行 java.lang.Object 类的初始化块,开始执行 java.lang.Object 的构造器,依次向下执行其父类的初始化块,开始执行其父类的构造器……最后才执行该类的初始化块和构造器,返回该类的对象。

虽然 Java 允许一个类中定义多个的普通初始化块,但这没有任何意义,所以如果要使用初始化块的话定义一个就行了。


初始化块和构造器的区别

初始化块总是在构造器之前执行。虽然它们的作用非常相似,但依然存在一些差异的。

与构造器不同的是,初始化块是一段固定执行的代码,它不能接受任何参数。因此,如果有一段初始化的代码对所有对象完全相同,且无须接收任何参数,就可以把这段初始化代码提取到初始化块中。

通过把多个构造器中的相同代码提取到初始化块中,能更好地提高初始化代码的复用,提高整个应用的可维护性。


静态初始化块

初始化块的修饰符只能是static,使用 static 修饰的初始化块被称为静态初始化块

静态初始化块,也属于类的静态成员,因此静态初始化块不能访问非静态成员(包括实例Field和实例方法)。静态初始化块用于对整个类进行初始化处理,通常用于对类Field执行初始化处理。

系统将在类初始化阶段执行静态初始化块,而不是在创建对象时才执行。因此,静态初始化块总是比普通初始化块先执行。与普通初始化块类似的是,系统在类初始化阶段不仅会执行本类的静态初始化块,还会一直上溯到 java.lang.Object 类(如果它包含静态初始化块),从上往下依次执行其父类的静态初始化块……最后才执行该类的静态初始化块。经过这个过程,才完成了该类的初始化。而只有类完成初始化以后,才可以在系统中使用这个类,包括访问这个类的类Field、类方法,或者用这个类来创建实例。


第五章 面向对象(下)

包装类

Java 是面向对象的编程语言,但它也包含了 8 种基本数据类型。基本数据类型的数据不具备“对象”的特性:没有Field、方法可以被调用。

所有引用类型的变量都继承了Object类,都可当成 Object 类型变量使用,但基本数据类型的变量却不可以。为了解决这个问题,Java 提供了包装类(Wrapper Class),可以把 8 个基本类型的值包装成对象使用。

把基本数据类型变量 包装成 对应的包装类对象 是通过对应包装类的构造器来实现的,不仅如此,8个包装类中除了 Character 之外,还可以通过传入一个字符串来构建包装类对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyClass {

public static void main(String[] args) {
boolean b1 = true;
int i1 = 5;

Boolean _b = new Boolean(b1);
Integer _i = new Integer(i1);
Float _f = new Float("3.14");

// 取出包装类对象里的值
boolean b2 = _b.booleanValue();
int i2 = _i.intValue();
float f2 = _f.floatValue();
}
}

可能你会觉得,这样的转换有些繁琐。但从 JDK 1.5 开始提供了自动装箱(Autoboxing)自动拆箱(AutoUnboxing)功能,即可以把一个基本类型变量直接赋给对应的包装类变量或 Object 变量(自动装箱),也可以把包装类对象直接赋给一个对应的基本类型变量。


基本类型变量与字符串的转换


toString( )方法

toString()方法是 Object 类里的一个实例方法,而所有的 Java 类都是 Object 类的子类,因此所有的 Java 对象都具有 toString() 方法。

不仅如此,所有的 Java 对象都可以和字符串进行连接运算,也可以使用System.out.println()进行输出。当进行上面的操作时,系统会自动调用 Java 对象的 toString()方法,使用其返回的字符串。

Object 类的toString方法是一个“自我描述”的方法,它总是返回该对象实现类的“类名@hashCode”值。但是这个返回值并不能真正实现“自我描述”的功能,这时可以对这个方法进行重写。


==和equals的区别

Java 程序中判断两个变量是否相等有两种方式:一种是使用==运算符,另一种是使用equals方法。

  • 对于基本类型变量来说,它们并没有equals方法,只能使用==判断两个变量的值是否相等。
  • 对于引用类型变量来说,==运算符是判断两个引用变量是否指向内存中的同一个对象,也就是比较对象的内存地址;而equals是比较两个对象的值是否相等(String类,Integer类等等)。

需要知道的是,equals 方法是 Object 类的一个实例方法。在 Object 类中equals方法和==没有任何区别,都是判断两个变量是否指向同一个对象。而String类,Integer类等等一些类,是重写了equals方法,才使得equals和“==”不同。

所以,当自己创建类时,想要自定义相等的标准,必须重写equals方法。


final修饰符

Java 提供了final关键字来修饰变量、方法和类。系统不允许为 final变量重新赋值,子类不允许覆盖父类的final方法,不允许继承final类。

  • final 成员变量必须由程序员显式地指定初始值,系统不会对 final 成员变量进行隐式初始化。对于 final 修饰的类 Field,必须在声明该Field时或在静态初始化块中指定初始值;对于 final 修饰的实例 Field,必须在声明该Field时、普通初始化块或构造器中指定初始值。

  • 前面说过,系统不会为局部变量执行隐式初始化,必须由程序员显式指定。对于 final 修饰的局部变量,可以在声明时指定初始值,也可以在后面的代码中对其赋值,但只能一次。

  • final 修饰基本类型变量时,表示变量的值不能被改变;final 修饰引用类型变量时,表示该变量所引用的地址不能被改变,即一直引用同一个对象,但这个对象是可以改变的。

  • 当定义 final 变量时就为该变量指定了初始值,而且该初始值可以在编译时就被确定下来,那么这个变量就变成了“宏变量”。编译器会把程序中所有用到该变量的地方直接替换成该变量的值。


抽象类与抽象方法

Java 中使用abstract修饰符来定义抽象类和抽象方法。有抽象方法的类必须定义成抽象类,但抽象类里可以没有抽象方法。

抽象类不能被实例化,只能当作父类被其他子类继承。抽象方法没有函数体,必须由子类提供实现(即重写)。

abstract不能同时使用的关键字:

  1. final和abstract不能同时使用,因为它们是对立的。
  2. static和abstract不能同时修饰某个方法,因为如果一个抽象方法被定义成静态方法,通过类名调用该方法将出现错误。
  3. private和abstract不能同时修饰某个方法,因为抽象方法必须被子类重写才有意义,而子类不能访问和重写父类的 private 方法。

接口(interface)

上面说到,抽象类既可以包含抽象方法,也可以普通方法。而接口(interface)是一种更彻底的抽象,接口里的所有方法都是抽象方法。

接口定义的是多个类共同的公共行为规范,故它里面通常是定义一组公用方法。基本语法如下:

1
2
3
4
5
[修饰符] interface 接口名 extends 父接口1,父接口2...
{

零个到多个常量定义...
零个到多个抽象方法定义...
}

修饰符可以是 public 或者省略,如果省略了 public 访问控制符,则默认采用包权限访问控制符。另外,与类继承不同的是,接口继承中一个接口可以有多个直接父接口(接口只能继承接口而不能继承类)。

由于接口是一种规范,因此接口里不能包含构造器和初始化块。接口里可以包含3种成员: Field(只能是常量)、方法(只能是抽象方法)、内部类(包括内部接口、枚举)。

  • 接口里所有成员都是public访问权限。
  • 接口里的常量 Field 是使用public static final修饰符来修饰,不管定义时是否指定。
  • 接口里的方法是自动使用public abstract修饰符修饰,不管定义方法时是否指定。
  • 接口里的内部类(接口、枚举类)是自动使用public static修饰符修饰,不管定义时是否指定。

接口不能用于创建实例,其主要用途是被实现类实现。实现使用implements关键字:

1
2
3
4
[修饰符] class 类名 extends 父类 implements 接口1,接口2...
{

// 类体部分
}

一个类只能有一个直接父类,但一个类可以实现多个接口。实现接口与继承父类相似,也可以获得所实现的接口里定义的成员,因此可以把实现接口理解为一种特殊的继承。


接口与抽象类的比较

相同点:

  • 接口和抽象类都不能被实例化,它们都用于被其他类实现或继承。
  • 接口和抽象类都可以包含抽象方法,实现接口或继承抽象类的普通子类都必须实现这些抽象方法。

不同点:

  • 接口里只能包含抽象方法,而抽象类既可以抽象方法也可以包含普通方法,还可以没有抽象方法。
  • 接口里不能定义静态方法(因为全部是抽象方法),抽象类里可以定义静态方法。
  • 接口里只能定义静态常量 Field,而抽象类既可以定义普通 Field,也可以定义静态常量 Field。
  • 接口里不包含构造器和初始化块,而抽象类里完全可以包含。
  • 一个类最多只能有一个直接父类,但却可以直接实现多个接口(弥补Java单继承的不足)。

内部类

在Java类里只能包含5种成员:Field、方法、构造器、初始化块、内部类(包括接口和枚举类)。前四种类成员已经介绍过了,下面介绍一下内部类。

内部类也叫嵌套类,语法格式:

1
2
3
4
public class OuterClass
{

// 此处可以定义内部类
}

通常,我们把内部类作为成员内部类来定义,而不是作为局部内部类(在方法里定义的内部类)。

非静态内部类里不允许定义静态成员,而静态内部类里可以定义静态成员,也可以定义非静态成员。

根据静态成员不能访问非静态成员的规则,外部类的静态方法不能使用非静态内部类,静态内部类也不能访问外部类的非static成员。


枚举类

枚举类是一种不能自由创建对象的类,它的对象在定义类时已经固定下来。枚举类特别适合定义像行星、季节这样的类,它们能创建的实例是有限且确定的。

在 Java 1.5 以前,要定义一个枚举类,必须手动去实现。下面就是一个 Season 枚举类的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Season {

private final String name;
private final String description;

public static final Season SPRING = new Season("春天","春暖花开");
public static final Season SUMMER = new Season("夏天","夏日炎炎");
public static final Season FALL = new Season("秋天","秋高气爽");
public static final Season WINTER = new Season("冬天","围炉赏雪");

// 私有化构造器
private Season(String name, String description) {
this.name = name;
this.description = description;
}

// 只为两个 Field 提供 getter 方法
public String getName() {
return name;
}
public String getDescription() {
return description;
}
}

上面的 Season 类是一个不可变类,它只能创建4种对象,可以通过Season.SPRING的方式来取得 Season 对象。

Java 1.5 新增了一个enum关键字,用以定义枚举类:

1
2
3
public enum Season {
SPRING,SUMMER,FALL,WINTER;
}

枚举类(enum)是一种特殊的类,它一样可以有自己的 Field、方法和构造器,可以实现一个或者多个接口。因为它特殊,所以有几点需要注意:

  1. enum定义的枚举类默认继承了java.lang.Enum类,而不是继承 Object 类。
  2. enum定义的非抽象的枚举类默认使用 final 修饰,因此枚举类不能派生子类。
  3. 枚举类的构造器默认使用且只能使用private访问控制符修饰。
  4. 枚举类的所有实例必须在枚举类的第一行显式列出,并且系统会自动添加public static final修饰。


第六章 Java基础类库

1、与用户交互

main 方法详解

我们知道Java程序的入口是public static void main(String[] args)

  • public:因为main方法是由 JVM 调用,所以使用 public 修饰符把这个方法暴露出来。

  • static:JVM 调用主方法时,是直接通过该类名来调用主方法,所以使用 static 修饰。

  • void:因为主方法被JVM调用,该方法的返回值将返回给JVM,这没有任何意义,因此main方法没有返回值。

main方法有一个字符串数组形参,当通过命令行运行Java程序时,在类名后紧跟一个或多个字符串,JVM就会把这些字符串依次赋给args数组元素,例如下面一段代码:

1
2
3
4
5
6
7
8
public class MyClass {
public static void main(String[] args) {
System.out.println(args.length); // 输出args数组长度
for(String arg : args) { // 遍历
System.out.println(arg);
}
}
}

在CMD窗口中编译、运行:

1
2
3
4
5
6
7
8
9
10
11
[master@localhost ~]$ javac MyClass.java     // 编译
[master@localhost ~]$ java MyClass // 运行,无参数
0
[master@localhost ~]$ java MyClass first second third // 运行,后跟三个参数
3
first
second
third
[master@localhost ~]$ java MyClass "first second third" // 运行,双引号内是一个整体
1
first second third

从上面的运行结果可以看出,如果某参数本身包含了空格,则应该将该参数用双引号(“”)括起来,否则JVM会把这个空格当成参数分隔符。


使用 Scanner 获取键盘输入

Scanner是一个基于正则表达式的文本扫描器,它可以从文件、输入流、字符串中解析出基本类型值和字符串值。

Scanner主要提供了两个方法来扫描输入:

  • hasNextXxx():是否还有下一个输入项,其中 Xxx 可以是 Int、Long 等。如果判断字符串则直接用 hasNext。

  • nextXxx():获取下一个输入项,Xxx 含义同上。获取字符串直接用 next。

下面是用Scanner读取键盘输入的示例:

1
2
3
4
5
6
7
8
9
10
public class MyClass {
public static void main(String[] args) {
// System.in 代表标准输入,即键盘输入
Scanner sc = new Scanner(System.in);
// 判断是否还有下一个输入项
while(sc.hasNext()) {
System.out.println("键盘输入的内容是:" + sc.next());
}
}
}

默认情况下,Scanner使用空格作为多个输入项之间的分隔符。如果我们想一次读取一行,可以把Scanner的分隔符设置为回车符,使用函数sc.useDelimiter("\n")。事实上,Scanner提供了两个更简单的方法逐行读取,即hasNextLine()nextLine()

Scanner还可以读取文件输入,只要在创建Scanner对象时传入一个File对象:

1
2
3
4
5
6
7
8
9
10
public class MyClass {
public static void main(String[] args) throws IOException {
// 读取文件
Scanner sc = new Scanner(new File("D://MyClass.java"));
// 逐行输出
while(sc.hasNextLine()) {
System.out.println(sc.nextLine());
}
}
}


使用 BufferedReader 获取键盘输入

Scanner是Java 5新增的工具类。在此之前,程序通常通过BufferedReader类来读取键盘输入。

BufferedReader 是Java IO流中的一个字符缓存流,它必须建立在另一个字符流的基础之上。但标准输入(System.in)是字节流,程序需要使用转换流InputStreamReader将其包装成字符流。故通过 BufferedReader 读取键盘输入的代码如下:

1
2
3
4
5
6
7
8
9
10
public class MyClass {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String line = null;
// 逐行读取
while((line = br.readLine()) != null) {
System.out.println("键盘输入的是:" + line);
}
}
}

当然,BufferedReader 也可以用来读取文件:

1
2
3
4
5
6
7
8
9
10
11
public class MyClass {
public static void main(String[] args) throws IOException {
FileReader fr = new FileReader("D://MyClass.java");
BufferedReader br = new BufferedReader(fr);
String line = null;
// 逐行读取
while((line = br.readLine()) != null) {
System.out.println(line);
}
}
}


2、系统相关

Java程序在不同的操作系统上运行时,可能需要取得平台相关的属性,或者调用平台命令完成特定功能。Java提供了System类和Runtime类来与程序的运行平台进行交互。

System类

System 类代表了当前 Java 程序的运行平台,程序不能创建该类对象,但可以调用它的类Field和类方法。System类提供了:

  • 标准输入(in)/标准输出(out)/错误输出(err),它们都是类Field。

  • 访问程序所在平台的环境变量、系统属性的方法。请看示例代码

  • 加载文件和动态链接库的方法(load()/loadLibrary())。

  • 获取系统当前时间。

获取环境变量/系统属性的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyClass {
public static void main(String[] args) throws IOException {
// 获取系统所有的环境变量
Map<String,String> env = System.getenv();
for(String name : env.keySet()) { // 遍历输出
System.out.println(name + "--->" + env.get(name));
}
System.out.println(System.getenv("JAVA_HOME"));

// 获取系统属性
System.out.println(System.getProperty("os.name")); // Linux
System.out.println(System.getProperty("os.arch")); // amd64
System.out.println(System.getProperty("user.timezone")); // Asia/Shanghai
System.out.println(System.getProperty("user.name")); // SongLee
}
}

如果想查看能够获取哪些属性,我们可以获取所有的系统属性并保存到本地文件中:

1
2
Properties props = System.getProperties();
props.store(new FileOutputStream("D://props.txt"), "System Properties");

至于 System 类中两个获取系统当前时间的方法:currentTimeMillis()nanoTime(),它们都返回 long 型整数(与1970.1.1的时间差),前者以毫秒为单位,后者以纳秒为单位。


Runtime类

Runtime类代表Java程序的运行时环境,每个Java程序都有一个与之对应的Runtime实例,通过getRuntime()方法可获取该Runtime对象。Runtime类提供了:

  • 获取JVM相关信息,如处理器数量、内存信息等。

  • 加载文件和动态链接库的方法(load()/loadLibrary()

  • 运行操作系统命令(exec()

1
2
3
4
5
6
7
8
9
10
public class MyClass {
public static void main(String[] args) {
Runtime rt = Runtime.getRuntime();

System.out.println("处理器数量:" + rt.availableProcessors());
System.out.println("空闲内存数:" + rt.freeMemory());
System.out.println("总的内存数:" + rt.totalMemory());
System.out.println("最大可用内存数:" + rt.maxMemory());
}
}

3、常用类

Object类

Object类是所有类/数组/枚举类的父类。当定义一个类时没有使用extends时,则该类默认继承Object父类。 Object类提供了几个常用方法:

  • boolean equals(Object obj):判断两个对象地址是否相同
  • protected void finalize():强制执行垃圾回收
  • Class<?> getClass():返回该对象的运行时类
  • int hashCode():返回该对象的hashCode值
  • String toString():返回”运行时类名@十六进制hashCode值”

String、StringBuffer、StringBuilder的区别

  • String类:它是不可变类,一旦创建,字符序列不可改变。

  • StringBuffer类:它代表一个字符序列可变的字符串,通过append()/insert()/reverse()等方法可以改变对象的字符序列,并且线程安全。一旦生成了最终想要的字符串,可以toString()导出String对象。

  • StringBuilder类:和StringBuffer相同,只不过StringBuilder不是线程安全的,所以性能略高。


Math类

Math类中提供了大量的静态方法用来完成复杂的数学运算,请自行查询API。另外,Math类还有两个静态变量:PI(π)和E(e)

1
2
3
4
5
6
public class MyClass {
public static void main(String[] args) {
System.out.println(Math.PI); // 3.141592653589793
System.out.println(Math.E); // 2.718281828459045
}
}


Random 与 ThreadLocalRandom类

  1. Random类:用于生产一个伪随机数,默认使用当前时间作为种子,也可以指定种子。

  2. ThreadLocalRandom类:它是Random的增强版,在并发环境下线程安全。且需通过current()静态方法获取实例对象。

1
2
3
4
5
6
7
8
9
import java.util.concurrent.ThreadLocalRandom;

public class MyClass {
public static void main(String[] args) {
ThreadLocalRandom rand = ThreadLocalRandom.current();
int num1 = rand.nextInt(4, 20);
double num2 = rand.nextDouble(4.0, 20.0);
}
}

BigDecimal类

我们知道float、double两种基本浮点类型在进行运算时容易发生精度丢失。为了能精确表示和计算浮点数,Java提供了BigDecimal类。该类有add/subtract/multiply/divide/pow等方法……

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.math.BigDecimal;

public class MyClass {
public static void main(String[] args) {
System.out.println("0.05 + 0.01 = " + (0.05+0.01));

BigDecimal f1 = BigDecimal.valueOf(0.05);
BigDecimal f2 = BigDecimal.valueOf(0.01);
System.out.println("0.05 + 0.01 = " + f1.add(f2));
}
}

// 输出:
// 0.05 + 0.01 = 0.060000000000000005
// 0.05 + 0.01 = 0.06


时间和日期类

  • Date类:java.util.Date类是一个设计糟糕的类,而且从JDK 1.0就开始存在,已经过时,官方推荐尽量少用。

  • Calendar类:这是一个抽象类,不能创建对象。但可以通过几个静态方法getInstance()方法获取GregorianCalendar类的对象,可以指定TimeZone、Locale,不指定则使用系统默认的。

  • java.time.*类:Java 8专门新增了一个java.time包,包含了Clock/Duration/LocalDate/LocalTime/Year/Month/DayOfWeek等等类。


正则类

在String类中提供了几个正则匹配的方法,比如matches(String regex)split(String regex)。除此之外,Java还提供了Pattern和Matcher两个类专门用于正则表达式。

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.util.regex.Pattern;
import java.util.regex.Matcher;

public class MyClass {
public static void main(String[] args) {
Pattern pattern = Pattern.compile("\\s.*a");
Matcher match = pattern.matcher("Hello Java!");

while(match.find()) {
System.out.println(match.group()); // 输出 Java
}
}
}