目录
方法重载,就是在同一个作用域内,方法名相同但参数个数或者参数类型不同的方法。
(1)使用super关键字访问或调用父类的非私属性或非私有方法
Serializable接口和Externalizable接口实现序列化机制的主要区别
基于学过c语言或者基于学过c++的学习笔记。
1.java的基本运行
public class HelloWorld //public说明这个类是公共的,一个源文件只能用一个public //且当前java文件前缀名必须跟这个public的类名一致。 { public static void main(String[] args){ System.out.println("Hello World!!!"); } } //class 是类的修饰符,HelloWorld是类名 正常是 class 类名 { ..... }
javac就是java的编译器,javac对源程序编译(javac HelloWorld.java),产生字节码文件,如HelloWorld.class。然后java HellowWorld命令启动java的虚拟运行程序,java虚拟机首先将.class的字节码文件加载入内存,这个过程称为类加载,由类加载器完成。然后java虚拟机针对加载到内存中的java类进行解释进行解释执行,输出运行成果。
从上面可见,不同的平台只要装相应版本的java编译器、java虚拟机即可运行相同的java程序。
2、基本格式
java中严格大小写。
char类型+整型==整型
同一个字符串不能写在2行。想要写同一行
System.out.println("dwadwa"+"dwdawd");
可以这样拆分。
注释
//单行注释 /*多行注释 /* /** 文档注释,可以用javadoc命令提取文档注释生成帮助文档。 */ 多行注释里面可以嵌套使用单行注释。 多行不能嵌套多行
标识名
变量名、方法名、参数名、包名、类名。
可以由字母、数字、_、$组成,但不能以数字开头,也不能是java现有的关键字。
命名规范:
1、包名一律小写;
2、类名和接口名每个单词的首字母都大写。
3、常量名所有字母都大写,单词之间用下画线。
4、变量名和方法名的第一个单词首字母小写,第二个单词开始每个单词首字母大写。
5、在程序中,应该尽量使用有意义的英文单词。
关键字
常量
整型常量
二进制:
jdk7开始,运用用字面值表示二进制,前面以0b或0B开头,例子:0b10100110
八进制:
0开头。如0342
十进制:
第一位不能为0。如231。
十六进制:
0x或0X开头,0-9,A-F。其中字母不区分大小写。0xAbd92
浮点数:
分float和double,前者F或f结尾,后者D或d结尾。但也可以不加,默认double
另外可以以指数形式5.022e+23f
字符常量:
表示一个字符,一个字符常量用''括起来。包括英文字母、数字、标点符号、转义字符
因为java是Unicode字符集,空为\u0000。'\u0000'里面没有任何字符。
字符串常量
一串连续的字符,一个字符串常量用""括起来。可以包含多个字符也可以不包含字符,长度为0。
布尔
分true和false。
null值
表示为空。
变量
在程序运行期间,随时可能产生一些临时数据,应用程序会将这些数据保存在内存单元中,每个内存单元都用一个标识符标识,这些用于标识内存单元的标识符就称为变量,内存单元中存储的数据就是变量的值。
int x = 0,y; y = x+3;
整型变量:
整数类型分为4种不同的类型,分别是字节型(byte)、短整型(short)、整型(int)和长整型(long)
注意:在为一个long类型的变量赋值时,所赋值的后面要加上字母L(或小写l),说明赋值为long类型。如果赋的值未超出int类型的取值范围,则可以省略字母L(或小写l)。
浮点变量:
double比float更精确
float类型变量值后面必须要加上F或f,double类型变量值后面可以加上D或d,也可以不加。也可以为一个浮点数类型变量赋予一个整数数值,JVM会自动将整数数值转换为浮点类型的值。
字符变量:
在Java中,字符型变量用char表示,用于存储一个单一字符。Java中每个char类型的字符变量都会占用2个字节。在给char类型的变量赋值时,需要用一对英文半角格式的单引号(' ')把字符括起来,如'a'。
char c = 'a'; // 为一个char类型的变量赋值字符a char ch = 97; // 为一个char类型的变量赋值整数97,相当于赋值字符a
布尔变量:
使用boolean定义布尔型变量,布尔型变量只有true和false两个值。
类型转换
自动类型转换
自动类型转换也叫隐式类型转换,指的是两种数据类型在转换的过程中不需要显式地进行声明,由编译器自动完成。自动类型转换必须同时满足以下两个条件: (1)两种数据类型彼此兼容。 (2)目标类型的取值范围大于源类型的取值范围。
byte b = 3; int x = b; 使用byte类型的变量b为int类型的变量x赋值,由于int类型的取值范围大于byte类型的取值范围,编译器在赋值过程中不会造成数据丢失,所以编译器能够自动完成这种转换,在编译时不报告任何错误。
(1)整数类型之间可以实现转换。 例如,byte类型的数据可以赋值给short、int、long类型的变量;short、char类型的数据可以赋值给int、long类型的变量;int类型的数据可以赋值给long类型的变量。 (2)整数类型转换为float类型。 例如,byte、char、short、int类型的数据可以赋值给float类型的变量。 (3)其他类型转换为double类型。 例如,byte、char、short、int、long、float类型的数据可以赋值给double类型的变量。
强制类型转换
强制类型转换也叫显式类型转换,指的是两种数据类型之间的转换需要进行显式地声明。当两种类型彼此不兼容,或者目标类型取值范围小于源类型时,自动类型转换无法进行,这时就需要进行强制类型转换。
转换格式:(目标类型)值
注意,这种目标类型小于源类型的时候,极容易出现精度丢失。
变量b本身的值为298,然而在赋值给变量a后,a的值为42。 出现这种现象的原因是,变量b为int类型,在内存中占用4个字节; byte类型的数据在内存中占用1个字节,当将变量b的类型强转为byte类型后, 前面3个高位字节的数据丢失,数值发生改变。
变量在表达式中进行运算时,可能发生自动类型转换,这就是表达式数据类型的自动提升。解决数据自动提升类型的方法,就是进行强制类型转换。
然而在使用+=、-=、*=、/=、%= 运算符进行赋值时,强制类型转换会自动完成,程序不需要做任何显式地声明。
运算符
在进行除法运算时,当除数和被除数都为整数时,得到的结果也是一个整数。如果除法运算有小数参与,得到的结果会是一个小数。
在进行取模(%)运算时,运算结果的正负取决于被模数(%左边的数)的符号,与模数(%右边的数)的符号无关。
int x, y, z; x = y = z = 5;// 为三个变量同时赋值 int x = y = z = 5;// 这样写是错误的
优先级
(1)三元运算符“?”和“:”是一对运算符,不能分开单独使用。 (2)三元运算符的优先级低于关系运算符与算术运算符,但高于赋值运算符。 (3)三元运算符可以进行嵌套,结合方向自右向左。 例如,a>b?a:c>d?c:d应该理解为a>b?a:(c>d?c:d),这也是三元运算符的嵌套情形,即三元表达式中的表达式2又是一个三元表达式。
---------
注意==对于对象,默认是比较内存地址(或者说引用)
3、选择与循坏结构
选择
if (判断条件1) { 执行语句1 } else if (判断条件2) { 执行语句2 } ... else if (判断条件n) { 执行语句n } else { 执行语句n+1 }
switch (表达式){ case 目标值1: 执行语句1 break; case 目标值2: 执行语句2 break; ...... case 目标值n: 执行语句n break; default: 执行语句n+1 break; }
循坏
while(循环条件){ 执行语句 ... } do { 执行语句 ... } while(循环条件);
for(初始化表达式; 循环条件; 操作表达式){ 执行语句 ... }
循坏嵌套,循坏break和continue
4.数组和方法
数组
int[] x;// 声明一个int[]类型的变量 x = new int[100];// 为数组x分配100个元素空间 Java提供了一个length属性, 在程序中可以通过“数组名.length”的方式获得数组的长度,即元素的个数。
在定义数组时只指定数组的长度,由系统自动为元素赋初值的方式称作动态初始化。在初始化数组时还有一种方式叫做静态初始化,就是在定义数组的同时就为数组的每个元素赋值。数组的静态初始化有以下两种方式。
类型[] 数组名 = new 类型[]{元素,元素,……}; 类型[] 数组名 = {元素,元素,元素,……};
下标越界
ArrayIndexOutOfBoundsException,出现这个异常的原因是该数组的长度为4,索引范围为0~3
在使用变量引用一个数组时,变量必须指向一个有效的数组对象, 如果该变量的值为null,则意味着没有指向任何数组, 此时通过该变量访问数组的元素会出现空指针异常。
二维数组
数据类型[][] 数组名 = new数据类型[行的个数][列的个数];
数据类型[][] 数组名 = new int[行的个数][];
数据类型[][] 数组名= {{第0行初始值},{第1行初始值},...,{第n行初始值}};
int[][] mp=new int[4][];
方法
修饰符 返回值类型 方法名(参数类型 参数名1,参数类型 参数名2,...){ 执行语句 ... return 返回值; }
修饰符:方法的修饰符比较多,例如,对访问权限进行限定的修饰符,static修饰符,final修饰符等
方法重载,就是在同一个作用域内,方法名相同但参数个数或者参数类型不同的方法或参数顺序不一样。
// 下面的方法实现了两个整数相加 public static int add(int x, int y) { return x + y; } // 下面的方法实现了三个整数相加 public static int add(int x, int y, int z) { return x + y + z; } // 下面的方法实现了两个小数相加 public static double add(double x, double y) { return x + y; }
5.类与对象
类的定义
类中可以定义成员变量和成员方法,其中,成员变量用于描述对象的特征,成员变量也被称作对象的属性;成员方法用于描述对象的行为,可简称为方法
class 类名{ 成员变量; 成员方法; }
class Student { String name; // 声明String类型的变量name int age; // 声明int类型的变量age String sex; // 声明String类型的变量sex // 定义 read () 方法 void read() { System.out.println("大家好,我是" + name + ",我在看书!"); } }
在成员方法read()中可以直接访问成员变量name。
定义在类中的变量被称为成员变量,定义在方法中的变量被称为局部变量。如果在某一个方法中定义的局部变量与成员变量同名,这种情况是允许的,此时,在方法中通过变量名访问到的是局部变量,而并非成员变量。
变量类型 是否自动初始化默认值 成员变量 是(如 0
/null
)局部变量 否(必须手动初始化)
对象创建
类名 对象名称 = null; 对象名称 = new 类名(); 类名 对象名称 = new 类名();
new关键字创建的对象是在堆内存分配空间
对象访问
对象名称.属性名 对象名称.方法名
引用数据类型
类属于引用数据类型,引用数据类型就是指内存空间可以同时被多个栈内存引用。下面通过一个案例详细讲解对象的引用传递,具体代码如下所示
1class Student { 2 String name; // 声明姓名属性 3 int age; // 声明年龄属性 4 void read() { 5 System.out.println("大家好,我是"+name+",年龄"+age); 6 } 7} 8class Example02 { 9 public static void main(String[] args) { 10 Student stu1 = new Student(); //创建stu1对象并实例化 11 Student stu2 = null; //创建stu2对象,但不对其进行实例化 12 stu2 = stu1; //stu1给stu2分配空间使用权 13 stu1.name = "小明"; //为stu1对象的name属性赋值 14 stu1.age = 20; 15 stu2.age = 50; 16 stu1.read(); //调用对象的方法 17 stu2.read(); 18 } 19}
stu2对象获得了stu1对象的堆内存空间的使用权
实际上所谓的引用传递,就是将一个堆内存空间的使用权给多个栈内存空间使用,每个栈内存空间都可以修改堆内存空间的内容。
一个栈内存空间只能指向一个堆内存空间,如果想要再指向其他堆内存空间,就必须先断开已有的指向才能分配新的指向。
访问控制权限
private、default、protected和public。
(1)private:private属于私有访问权限,用于修饰类的属性和方法, 也可以修饰内部类。 类的成员一旦使用了private关键字修饰,则该成员只能在本类中进行访问。 (2)default:default属于默认访问权限,如果一个类中的属性或方法没有任何的访问权限声明, 则该属性或方法就是默认的访问权限,默认的访问权限可以被本包中的其他类访问 ,但是不能被其他包的类访问。 (3)protected:protected属于受保护的访问权限。 如果一个类中的成员使用了protected访问权限,则只能被本包及不同包的子类访问。 (4)public:public属于公共访问权限。如果一个类中的成员使用了public访问权限, 则该成员可以在所有类中被访问,不管是否在同一包中。
public class Test { public int aa; //aa可以被所有的类访问 protected boolean bb; //bb可以被所有子类以及本包的类访问 void cc() { //默认访问权限,能在本包范围内问 System.out.println("包访问权限"); } //private权限的内部类,即这是私有的内部类,只能在本类中访问 private class InnerClass { } }
外部类的访问权限只能是public或default,所以Test类只能使用public修饰或者不写修饰符。局部成员是没有访问权限控制的,因为局部成员只在其所在的作用域内起作用,不可能被其他类访问到,如果在程序中这样编写代码,编译器会报错。
如果一个Java源文件中定义的所有类都没有使用public修饰, 那么这个Java源文件的文件名可以是一切合法的文件名; 如果一个源文件中定义了一个public修饰的类, 那么这个源文件的文件名必须与public修饰的类名相同。
类的封装
指将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象的内部信息,而是通过该类提供的方法实现对内部信息的操作访问。
封装的具体实现过程是,在定义一个类时,将类中的属性私有化,即使用private关键字修饰类的属性,私有属性只能在它所在的类中被访问。
如果外界想要访问私有属性,需要提供一些使用public修饰的公有方法,其中包括用于获取属性值的getXxx()方法(也称为getter方法)和设置属性值的setXxx()方法(也称为setter方法)。如果对属性的范围等有限制,我们可以在setxxx方法中用if进行判断。
1 public String getName() { 2 return name; 3 } 4 public void setName(String name) { 5 this.name = name; 6 } 7 public int getAge() { 8 return age; 9 } 10 public void setAge(int age) { 11 if(age < 0){ 12 System.out.println("您输入的年龄有误!"); 13 } else { 14 this.age = age; 15 } 16 }
构造方法
(1)构造方法的名称必须与类名一致。 (2)构造方法名称前不能有任何返回值类型的声明。 (3)不能在构造方法中使用return返回一个值,但可以单独写 return语句作为方法的结束。
1 class Student{ 2 public Student() { 3 System.out.println("调用了无参构造方法"); 4 } 5} 6public class Example05 { 7 public static void main(String[] args) { 8 System.out.println("声明对象..."); 9 Student stu = null; //声明对象 10 System.out.println("实例化对象..."); 11 stu = new Student(); //实例化对象 12 } 13}
1 class Student{ 2 private String name; 3 private int age; 4 public Student(String n, int a) { 5 name = n; 6 age = a; 7 System.out.println("调用了有参构造"); 8 } 9 public void read(){ 10 System.out.println("我是:"+name+",年龄:"+age); 11 } 12 } 13 public class Example06 { 14 public static void main(String[] args) { 15 Student stu = new Student("张三",18); // 实例化Student对象 16 stu.read();
与普通方法一样,构造方法也可以重载,在一个类中可以定义多个构造方法,但是需要每个构造方法的参数类型或参数个数不同。
1 class Student{ 2 private String name; 3 private int age; 4 public Student() { } 5 public Student(String n) { 6 name = n; 7 System.out.println("调用了一个参数的构造方法"); 8 } 9 public Student(String n,int a) { 10 name = n; 11 age = a; 12 System.out.println("调用了两个参数的构造方法"); 13 } 14 public void read(){ 15 System.out.println("我是:"+name+",年龄:"+age); 16 } 17 } 18 public class Example07 { 19 public static void main(String[] args) { 20 Student stu1 = new Student("张三"); 21 Student stu2 = new Student("张三",18); // 实例化Student对象 22 stu1.read(); 23 stu2.read(); 24 } 25 }
在Java中的每个类都至少有一个构造方法,如果在一个类中没有定义构造方法,系统会自动为这个类创建一个默认的构造方法,这个默认的构造方法没有参数,方法体中没有任何代码,所以Java中默认的构造方法在程序运行时什么也不做。
一旦为类定义了构造方法,系统就不再提供默认的构造方法了
在一个类中如果定义了有参的构造方法,最好再定义一个无参的构造方法。
注意:构造方法通常使用public进行修饰。
this
1 class Student { 2 private String name; 3 private int age; 4 // 定义构造方法 5 public Student(String name,int age) { 6 name = name; 7 age = age; 8 } 9 public String read(){ 10 return "我是:"+name+",年龄:"+age; 11 } 12 } 13 public class Example09 { 14 public static void main(String[] args) { 15 Student stu = new Student("张三", 18); 16 System.out.println(stu.read()); 17 } 18 }
构造方法参数名称与对象成员变量名称相同时,在name=name、age=age处,编译器无法确定name是哪个name,可能是局部也可能是成员变了。
Java提供了关键字this指代当前对象,通过this可以访问当前对象的成员
1 class Student { 2 private String name; 3 private int age; 4 public Student(String name,int age) {// 定义构造方法 5 this.name = name; 6 this.age = age; 7 } 8 public String read(){ 9 return "我是:"+name+",年龄:"+age; 10 } 11 } 12 public class Example10 { 13 public static void main(String[] args) { 14 Student stu = new Student("张三", 18); 15 System.out.println(stu.read()); 16 } 17 }
class Student { public void openMouth() { ... } public void read() { this.openMouth(); } } 在read()方法中使用this关键字调用了openMouth()方法。 需要注意的是此处的this关键字也可以省略不写。
构造方法是在实例化对象时被Java虚拟机自动调用,在程序中不能像调用其他成员方法一样调用构造方法,但可以在一个构造方法中使用“this(参数1,参数2…)”的形式调用其他的构造方法
1 class Student { 2 private String name; 3 private int age; 4 public Student () { 5 System.out.println("调用了无参的构造方法"); 6 } 7 public Student (String name,int age) { 8 this(); // 调用无参的构造方法 9 this.name = name; 10 this.age = age; 11 } 12 public String read(){ 13 return "我是:"+name+",年龄:"+age; 14 } 15 } 16 public class Example11 { 17 public static void main(String[] args) { 18 Student stu = new Student("张三",18); // 实例化 Student对象 19 System.out.println(stu.read()); 20 } 21 }
(1)只能在构造方法中使用this调用其他的构造方法,不能在成员方法中 通过this调用构造方法。 (2)在构造方法中,使用this调用其他构造方法的语句必须位于第一行, 且只能出现一次。 (3)不能在一个类的两个构造方法中使用this互相调用,下面是错的 class Student { public Student () { this("张三"); // 调用有参构造方法 System.out.println("无参的构造方法被调用了。"); } public Student (String name) { this(); // 调用无参构造方法 System.out.println("有参的构造方法被调用了。"); } }
代码块
代码块,简单来讲,就是用{}括号括起来的一段代码,根据位置及声明关键字的不同,代码块可以分为4种:普通代码块、构造块、静态代码块、同步代码块。
普通代码块
普通代码块就是直接在方法或是语句中定义的代码块
public class Example12 { public static void main(String[] args) { { int age = 18; System.out.println("这是普通代码块。age:"+age); } int age = 30; System.out.println("age:"+age); } }
每一对“{}”括起来的代码都称为一个代码块。Example12是一个大的代码块,在Example12代码块中包含了main()方法代码块,在main()方法中又定义了一个局部代码块,局部代码块对main()方法进行了“分隔”,起到了限定作用域的作用。 上述代码中的局部代码块中定义了变量age,main()方法代码块中也定义了变量age,但由于两个变量处在不同的代码块,作用域不同,因此并不相互影响。
构造代码块
构造代码块是直接在类中定义的代码块。
1 class Student{ 2 String name; //成员属性 3 { 4 System.out.println("我是构造代码块"); //与构造方法同级 5 } 6 //构造方法 7 public Student(){ 8 System.out.println("我是Student类的构造方法"); 9 } 10 } 11 public class Example12 { 12 public static void main(String[] args) { 13 Student stu1 = new Student(); 14 Student stu2 = new Student(); ......
(1)在实例化Student类对象stu1、stu2时,构造块的执行顺序大于构造方法 (这里和构造块写在前面还是后面没有关系)。 (2)每当实例化一个Student类对象,都会在执行构造方法之前执行构造块。
static关键字
静态属性
如果在Java程序中使用static修饰属性,则该属性称为静态属性(也称全局属性),静态属性可以使用类名直接访问 类名.属性名
1class Student { 2 String name; // 定义name属性 3 int age; // 定义age属性 4 String school = "A大学"; // 定义school属性 5 public Student(String name,int age){ 6 this.name = name; 7 this.age = age; 8 } 9 public void info(){ 10 System.out.println("姓名:" + this.name+",年龄:" +this. age+",学 11 校:" + school); 12 } 13} 14public class Example13 { 15 public static void main(String[] args) { 16 Student stu1 = new Student("张三",18); // 创建学生对象 17 Student stu2 = new Student("李四",19); 18 Student stu3 = new Student("王五",20); 19 stu1.info(); 20 stu2.info(); 21 stu3.info(); 22 //修改stu1对象的school的值 23 stu1.school = "B大学"; 24 System.out.println("修改stu1学生对象的学生信息为B大学后"); 25 stu1.info(); 26 stu2.info(); 27 stu3.info(); 28 } 29}
表明非静态属性是对象所有,改变当前对象的属性值,不影响其他对象的属性值。
可以使用static关键字修饰school属性,将其变为公共属性。这样,school属性只会分配一块内存空间,被Student类的所有对象共享,只要某个对象进行了一次修改,全部学生对象的school属性值都会发生变化。
1class Student { 2 String name; // 声明name属性 3 int age; // 声明age属性 4 static String school = "A大学"; // 定义school属性 ...... 14public class Example14 { 15 public static void main(String[] args) { 16 Student stu1 = new Student("张三",18); // 创建学生对象 17 Student stu2 = new Student("李四",19); 18 Student stu3 = new Student("王五",20); 19 stu1.info(); 20 stu2.info(); 21 stu3.info(); 22 stu1.school = "B大学"; 23 stu1.info(); 24 stu2.info(); 25 stu3.info(); ......
static关键字只能修饰成员变量,不能修饰局部变量,否则编译器会报错
静态方法
在不创建对象的情况下,通过类名就可以直接调用某个方法,这时就需要使用静态方法,要实现静态方法只需要在成员方法前加上static关键字。
类名.方法或者实例对象名.方法
class Student { private String name; // 声明name属性 private int age; // 声明age属性 private static String school = "A大学"; // 定义school属性 ... public static String getSchool() { return school; } public static void setSchool(String s) { school = s; } ... class Example15 { public static void main(String[] args) { Student stu1 = new Student("张三",18); // 创建学生对象stu1 Student stu2 = new Student("李四",19); // 创建学生对象stu2 Student stu3 = new Student("王五",20); // 创建学生对象stu3 System.out.println("----修改前----"); stu1.info(); ... System.out.println("----修改后----"); Student.setSchool("B大学"); //为静态属性school重新赋值 stu1.info(); ...
注意:静态方法只能访问静态成员,因为非静态成员需要先创建对象才能访问,即随着对象的创建,非静态成员才会分配内存。而静态方法在被调用时可以不创建任何对象。
静态代码块
用static关键字修饰的代码块称为静态代码块。当类被加载时(在主函数中第一次构造该对象时),静态代码块就会执行,由于类只加载一次,所以静态代码块只执行一次。在程序中,通常使用静态代码块对类的成员变量进行初始化。而且因为类加载肯定在实例对象前,所以静态代码块肯定优先于构造代码块。
1class Student{ 2 String name; //成员属性 3 { 4 System.out.println("我是构造代码块"); 5 } 6 static { 7 System.out.println("我是静态代码块"); 8 } 9 public Student(){ //构造方法 10 System.out.println("我是Student类的构造方法"); 11} 12} 13class Example16{ 14 public static void main(String[] args) { 15 Student stu1 = new Student(); 16 Student stu2 = new Student(); 17 Student stu3 = new Student(); ......
继承
在程序中,继承描述的是事物之间的所属关系,通过继承可以使多种事物之间形成一种关系体系。例如,猫和狗都属于动物,程序中便可以描述为猫和狗继承自动物
类的继承是指在一个现有类的基础上构建一个新的类,构建出来的新类被称作子类,现有类被称作父类。子类会自动继承父类的属性和方法,使得子类具有父类的特征和行为。
格式:
class 父类{ …… } class 子类 extends 父类{ …… }
1// 定义Animal类 2class Animal { 3 private String name; // 声明name属性 4 private int age; // 声明age属性 ......//省略getter/setter方法 18} 19// 定义Dog类继承Animal类 20class Dog extends Animal { 21 //此处不写任何代码 22} 23// 定义测试类 24 public class Example01 { 25 public static void main(String[] args) { 26 Dog dog = new Dog(); // 创建一个Dog类的对象 27 dog.setName("牧羊犬"); // 此时调用的是父类Animal中的setter方法 28 dog.setAge(3); // 此时调用的是父类Animal中的setter方法 29 System.out.println("名称:"+dog.getName()+",年龄:"+dog.getAge() 30 ); 31 } 32}
子类除了可以继承父类的属性和方法,也可以定义自己的属性和方法
1// 定义Animal类 2class Animal { ......//省略前面Animal类的书写 17} 18// 定义Dog类继承Animal类 19class Dog extends Animal { 20 private String color; // 声明color属性 21 public String getColor() { 22 return color; 23 } 24 public void setColor(String color) { 25 this.color = color; 26 } 27} 28// 定义测试类 29 public class Example02 { 30 public static void main(String[] args) { 31 Dog dog = new Dog(); // 创建并实例化dog对象 32 dog.setName("牧羊犬"); // 此时访问的是父类Animal中的setter方法 33 dog.setAge(3); // 此时访问的是父类Animal中的setter方法 34 dog.setColor("黑色"); // 此时访问的是Dog类中的setter方法 35 System.out.println("名称:"+dog.getName()+",年龄:"+dog.getAge()+", 36 颜色:"+dog.getColor()); 37 } 38}
注意:子类虽然可以通过继承访问父类的成员和方法,但不是所有的父类属性和方法都可以被子类访问。子类只能访问父类中public和protected修饰的属性和方法,父类中被private修饰的属性和方法不能被子类访问,如果父类和子类不在同一个包中,那么被默认修饰符default修饰的属性和方法也不能被子类访问。
(1)在Java中,类只支持单继承,不允许多继承。 下列是错误的。 class A{} class B{} class C extends A,B{} // C类不可以同时继承A类和B类 (2)多个类可以继承一个父类 下面是正确的 class A{} class B extends A{} //类B继承类A class C extends A{} //类C继承类A (3)在Java中,多层继承也是可以的,即一个类的父类可以再继承另外的父类。例如,C类继承自B类,而B类又可以继承自类A,这时,C类也可称作A类的子类。 下面是正确的 class A{} class B extends A{} // 类B继承类A,类B是类A的子类 class C extends B{} // 类C继承类B,类C是类B的子类,同时也是类A的子类 (4)在Java中,子类和父类是一种相对概念,一个类可以是某个类的父类,也可以是另一个类的子类。例如,在第(3)种情况中,B类是A类的子类,同时又是C类的父类。
重写
在继承关系中,子类会自动继承父类中定义的方法,但有时在子类中需要对继承的方法进行一些修改,即对父类的方法进行重写。在子类中重写的方法需要和父类被重写的方法具有相同的方法名、参数列表以及返回值类型。
1// 定义Animal类 2class Animal { 3 //定义动物叫的方法 4 void shout() { 5 System.out.println("动物发出叫声"); 6 } 7} 8// 定义Dog类继承Animal类 9class Dog extends Animal { 10 //重写父类Animal中的shout()方法 11 void shout() { 12 System.out.println("汪汪汪……"); 13 } 14} 15// 定义测试类 16public class Example03 { 17 public static void main(String[] args) { 18 Dog dog = new Dog(); // 创建Dog类的实例对象 19 dog.shout(); // 调用Dog类重写的shout()方法 20 } 21}
子类重写父类方法时,不能使用比父类中被重写的方法更严格的访问权限。例如,父类中的方法是public权限,子类的方法就不能是private权限。如果子类在重写父类方法时定义的权限缩小,则在编译时将出现错误。
super关键字
当子类重写父类的方法后,子类对象将无法访问父类中被子类重写过的方法。为了解决这个问题,Java提供了super关键字,使用super关键字可以在子类中访问父类的非私有方法、非私有属性以及构造方法。
(1)使用super关键字访问或调用父类的非私属性或非私有方法
super.属性 super.方法(参数1,参数2…)
......//省略定义Animal类 9 // 定义Dog类继承Animal类 10class Dog extends Animal { 11 // 重写父类Animal中的shout()方法,扩大了访问权限 12 public void shout() { 13 super.shout(); // 调用父类中的shout()方法 14 System.out.println("汪汪汪……"); 15 } 16 public void printName(){ 17 System.out.println("名字:"+super.name); // 访问父类中的name属性 18 } 19} 20// 定义测试类 21public class Example05 { 22 public static void main(String[] args) { 23 Dog dog = new Dog(); // 创建Dog类的对象 24 dog.shout(); // 调用Dog类重写的shout()方法 25 dog.printName(); // 调用Dog类中的printName()方法 26 } 27}
(2)使用super关键字调用父类中指定的构造方法
super(参数1,参数2…)
1// 定义Animal类 2class Animal { 3 private String name; 4 private int age; 5 public Animal(String name, int age) { // Animal类有参构造方法 6 this.name = name; 7 this.age = age; 8 } ...... 21 public String info() { 22 return "名称:"+this.getName()+",年龄:"+this.getAge(); 23 } 24} 25 // 定义Dog类继承Animal类 26class Dog extends Animal { 27 private String color; 28 public Dog(String name, int age, String color) { 29 super(name, age); //通过super关键字调用Animal类有两个参数的构造方法 30 this.setColor(color); 31 } ......//省略属性color的getter/setter方法 38 // 重写父类的info()方法 39 public String info() { 40 return super.info()+",颜色:"+this.getColor(); // 扩充父类中的方法 41 } 42} 43// 定义测试类 44public class Example06 { 45 public static void main(String[] args) { 46 Dog dog = new Dog("牧羊犬",3,"黑色"); // 创建Dog类的对象 47 System.out.println(dog.info()); 48 } 49}
注意:通过super()调用父类构造方法的代码必须位于子类构造方法的第一行,并且只能出现一次。
注意:this和super不可以同时出现,因为使用this和super调用构造方法的代码都要求必须放在构造方法的首行。
final关键字
修饰类
Java中使用final关键字修饰的类不可以被继承,也就是不能派生子类。
1 // 使用final关键字修饰Animal类 2 final class Animal { 3 } 4 // Dog类继承Animal类 5 class Dog extends Animal { 6 } 7 // 定义测试类 8 public class Example07 { 9 public static void main(String[] args) { 10 Dog dog = new Dog(); // 创建Dog类的对象 11 } 12 }
修饰方法
当一个类的方法被final关键字修饰后,该类的子类将不能重写该方法。
1 // 定义Animal类 2 class Animal { 3 // 使用final关键字修饰shout()方法 4 public final void shout() {} 5 } 6 // 定义Dog类继承Animal类 7 class Dog extends Animal { 8 // 重写Animal类的shout()方法 9 public void shout() {} 10 } 11 // 定义测试类 12 public class Example08 { 13 public static void main(String[] args) { 14 Dog dog=new Dog(); // 创建Dog类的对象 15 } 16 }
修饰变量
Java中被final修饰的变量为常量,常量只能在声明时被赋值一次,在后面的程序中,常量的值不能被改变。如果再次对final修饰的常量赋值,则程序会在编译时报错。
1public class Example09 { 2 public static void main(String[] args) { 3 final int AGE = 18; // 使用final关键字修饰的变量AGE第一次可以被赋值 4 AGE = 20; // 再次被赋值会报错 5 } 6}
注意:在使用final声明变量时,变量的名称要求全部的字母大写。如果一个程序中的变量使用public static final声明,则此变量将成为全局常量
public static final String NAME = "哈士奇";
抽象类
定义一个类时,常常需要定义一些成员方法用于描述类的行为特征,但有时这些方法的实现方式是无法确定的。例如,前面定义的Animal类中的shout()方法用于描述动物的叫声,但是不同的动物,叫声也不相同,因此在shout()方法中无法准确描述动物的叫声。 针对上面描述的情况,Java提供了抽象方法来满足这种需求。抽象方法是使用abstract关键字修饰的成员方法,抽象方法在定义时不需要实现方法体。并且不能实例化,必须通过子类实例化
abstract 返回值类型 方法名称(参数列表);
当一个类包含了抽象方法,该类就是抽象类。抽象类和抽象方法一样,必须使用abstract关键字进行修饰。
abstract class 抽象类名称{ 属性; 访问权限 返回值类型 方法名称(参数){ //普通方法 return [返回值]; } 访问权限 abstract 返回值类型 抽象方法名称(参数);//抽象方法,无方法体 }
抽象类的定义比普通类多了一个或多个抽象方法,其他地方与普通类的组成基本相同。
(1)包含抽象方法的类必须是抽象类。 (2)声明抽象类和抽象方法时都要使用abstract关键字修饰。 (3)抽象方法只需声明而不需要实现。 (4)如果非抽象类继承了抽象类,那么该类必须实现抽象类中的全部抽象方法。
1// 定义抽象类Animal 2abstract class Animal { 3 // 定义抽象方法shout() 4 abstract void shout(); 5} 6// 定义Dog类继承抽象类Animal 7class Dog extends Animal { 8 // 重写抽象方法shout() 9 void shout() { 10 System.out.println("汪汪..."); 11 } 12} 13// 定义测试类 14public class Example10 { 15 public static void main(String[] args) { 16 Dog dog = new Dog(); // 创建Dog类的对象 17 dog.shout(); // 通过dog对象调用shout()方法 18 } 19}
注意:使用abstract关键字修饰的抽象方法不能使用private关键字修饰,因为抽象方法必须要被子类实现,如果使用了private关键字修饰抽象方法,则子类无法实现该方法。
抽象类可以有构造方法
接口
概念:
接口是一种用来定义程序的协议,它用于描述类或结构的一组相关行为。接口是由抽象类衍生出来的一个概念,并由此产生了一种编程方式,可以称这种编程方式为面向接口编程。面向接口编程就是将程序的业务逻辑进行分离,以接口的形式去对接不同的业务模块。接口中不实现任何业务逻辑,业务逻辑由接口的实现类来完成。当业务需求变更时,只需要修改实现类中的业务逻辑,而不需要修改接口中的内容,以减少需求变更对系统产生的影响。
意义:
在Java中,使用接口的目的是为了克服单继承的限制,因为一个类只能有一个父类,而一个类可以同时实现多个父接口。在JDK 8之前,接口是由全局常量和抽象方法组成的。JDK 8对接口进行了重新定义,接口中除了抽象方法外,还可以定义默认方法和静态方法,默认方法使用default关键字修饰,静态方法使用static关键字修饰,且这两种方法都允许有方法体。
接口:
[public] interface 接口名 [extends 接口1,接口2...] { [public] [static] [final] 数据类型 常量名 = 常量; [public] [abstract] 返回值的数据类型 方法名(参数列表); [public] static 返回值的数据类型 方法名(参数列表){} [public] default 返回值的数据类型 方法名(参数列表){} }
不管写不写访问权限,接口中方法的访问权限永远是public。
接口本身不能直接实例化,接口中的抽象方法和默认方法只能通过接口实现类的实例对象进行调用。 实现类通过implements关键字实现接口,并且实现类必须重写接口中所有的抽象方法。 需要注意的是,一个类可以同时实现多个接口,实现多个接口时, 多个接口名需要使用英文逗号(,)分隔。
接口实现类:
修饰符 class 类名 implements 接口1,接口2,...{ ... }
第一、定义一个Animal接口,在Animal接口中定义了全局常量ID和全局常量NAME、 抽象方法shout()、info()和静态方法getID()。 第二、定义一个Action接口,在Action接口中定义了一个抽象方法eat(),用于输出信息“喜欢吃骨头”。 第三、定义一个Dog类,Dog类通过implements关键字实现了Animal接口和Action接口, 并重写了这两个接口中的抽象方法。 第四、使用Animal接口名直接访问了Animal接口中的静态方法getID(),输出编号信息。 第五、创建Dog类的对象dog,并通过dog对象调用了本类实现的Animal接口和Action接口中的info()方法、shout()方法,以及本类新增的eat()方法。
接口的实现类,必须实现接口中的所有抽象方法,否则程序编译报错。
如果在开发中一个子类既要实现接口又要继承抽象类
修饰符class 类名 extends 父类名 implements 接口1,接口2,... { ... }
第一、定义一个Animal接口,Animal接口中声明了全局常量NAME(名称)、 抽象方法shout()和抽象方法info()。 第二、定义一个抽象类Action,抽象类Action中定义了一个抽象方法eat()。 第三、定义一个Dog类,Dog类通过extends关键字继承了Action抽象类, 同时通过implements实现了Animal接口。 Dog类重写了Animal接口和Action抽象类中的所有抽象方法, 包括shout()方法、info()方法和eat()方法。 第四、创建一个Dog类对象dog,通过对象dog分别调用info()、shout()和eat()方法。
在Java中,接口是不允许继承抽象类的,但是允许接口继承接口,并且一个接口可以同时继承多个接口。
第一、定义一个Animal接口,Animal接口中声明了全局常量NAME(名称)、抽象方法info()。 第二、定义一个Color接口,Color接口中定义了一个抽象方法black()。 第三、定义一个接口Action并继承接口Animal和接口Color, 这样接口Action中就同时拥有Animal接口中的info()方法、 NAME属性和Color接口中的black()方法以及本类中的shout()方法。 第四、定义一个Dog类并实现了Action接口,这样Dog类就必须同时重写Animal接口、 中的抽象方法info()、Color接口中的抽象方法black()和Action接口中的抽象方法shout()。 第五、创建一个Dog类的对象dog,通过对象dog调用Dog类中定义的shout()方法以及Dog类中实现 自Action接口的info()方法和eat()方法。
-------
对于默认方法,如果多继承状态,比如接口A有默认的show方法,B也有默认的show。这时候类c,实现了AB接口,实现show的时候,要么自己重写一个,要么在重写的时候调用A.super.show()或者B.super.show()。
默认方法本身是为了不改动现有的实现类,给接口增加功能。
注意,接口继承接口,2个接口都有相同的默认方法,那么会默认调用子接口。当然,如果是实现类重写了方法,那就优先实现类的。
一个实现类如果实现了2个接口,这两个接口都继承了一个接口,且这个接口有一个默认方法。那么在实现类里面就调用这个默认方法,不会出错。
-----------
对于静态方法,接口中定义的静态方法,只能用该接口名.方法的方式调用。不能用子接口或实现类来调用,就是子接口和实现类不继承静态方法
默认方法不能同时是静态方法。
多态
多态是指不同类的对象在调用同一个方法时表现出的多种不同行为。
要实现一个动物叫声的方法,由于每种动物的叫声是不同的,因此可以在方法中接收一个动物类型的参数,当传入猫类对象时就发出猫类的叫声,传入犬类对象时就发出犬类的叫声。
Java中多态主要有以下两种形式。 (1)方法的重载。 (2)对象的多态(方法的重写)。
1// 定义抽象类Animal 2abstract class Animal { 3 abstract void shout(); // 定义抽象shout()方法 4} 5// 定义Cat类继承Animal抽象类 6class Cat extends Animal { 7 // 实现shout()方法 8 public void shout() { 9 System.out.println("喵喵……"); 10 } 11} 12// 定义Dog类继承Animal抽象类 13class Dog extends Animal { 14 // 实现shout()方法 15 public void shout() { 16 System.out.println("汪汪……"); 17 } 18} 19// 定义测试类 20public class Example14 { 21 public static void main(String[] args) { 22 Animal an1 = new Cat(); // 创建Cat对象,使用Animal类型的变量an1引用 23 Animal an2 = new Dog(); // 创建Dog对象,使用Animal类型的变量an2引用 24 an1.shout(); 25 an2.shout(); 26 } 27}
对象类型的转换
对象向上转型
对象向上转型,父类对象可以调用子类重写父类的方法,这样当需要新添功能时,只需要新增一个子类,在子类中对父类的功能进行扩展,而不用更改父类的代码,保证了程序的安全性。对于向上转型,程序会自动完成
注意,方法可以重写,但是对于属性,如果是以父类对象调用,则调用的是父类的属性。
对象向上转型:父类类型 父类对象 = 子类实例;
1// 定义Anmal类 2class Animal { 3 public void shout(){ 4 System.out.println("喵喵……"); 5 } 6} 7// 定义Dog类 8class Dog extends Animal { 9 // 重写shout()方法 10 public void shout() { 11 System.out.println("汪汪……"); 12 } 13 public void eat() { 14 System.out.println("吃骨头……"); 15 } 16} 17// 定义测试类 18public class Example15 { 19 public static void main(String[] args) { 20 Dog dog = new Dog(); // 创建Dog对象 21 Animal an = dog; // 向上转型 22 an.shout(); 23 } 24}
对象向下转型
向下转型一般是为了重新获得因为向上转型而丢失的子类特性。对象在进行的向下转型前,必须先进行向上转型,否则将出现对象转换异常。比如上面
这样的话,父类对象不能调用子类额外的方法和属性。
向下转型时,必须指明要转型的子类类型。
对象向下转型: 父类类型 父类对象 = 子类实例; 子类类型 子类对象 = (子类)父类对象;
17// 定义测试类 18public class Example16 { 19 public static void main(String[] args) { 20 Animal an = new Dog(); // 此时发生了向上转型,子类→父类 21 Dog dog = (Dog)an; // 此时发生了向下转型 22 dog.shout(); 23 dog.eat(); 24 } 25}
注意:在向下转型时,不能直接将父类实例强制转换为子类实例,否则程序会报错。
Dog dog = (Dog)new Animal();//编译错误
instanceof关键字
Java中可以使用instanceof关键字判断一个对象是否是某个类(或接口)的实例
对象 instanceof 类(或接口)
如果“对象”是指定的类的实例对象,则返回true,否则返回false。
1// 定义Animal类 2class Animal { 3 public void shout(){ 4 System.out.println("动物叫……"); 5 } 6} 17// 定义测试类 18public class Example17 { 19 public static void main(String[] args) { 20 Animal a1 = new Dog(); // 通过向上转型实例化Animal对象 21 System.out.println("Animal a1 = new Dog():"+(a1 instanceof Animal)); 22 System.out.println("Animal a1 = new Dog():"+(a1 instanceof Dog)); 23 Animal a2 = new Animal(); // 实例化Animal对象 24 System.out.println("Animal a2 = new Animal():"+(a2 instanceof Animal)); 25 System.out.println("Animal a2 = new Animal():"+(a2 instanceof Dog)); 26 } 27}
Object类
Java提供了一个Object类,它是所有类的父类,每个类都直接或间接继承了Object类,因此Object类通常被称为超类。当定义一个类时,如果没有使用extends关键字为这个类显式地指定父类,那么该类会默认继承Object类。
------------------------------------------------------
该类的常用方法:
1// 定义Animal类 2class Animal { 3 // 定义动物叫的方法 4 void shout() { 5 System.out.println("动物叫!"); 6 } 7} 8// 定义测试类 9public class Example18 { 10 public static void main(String[] args) { 11 Animal animal = new Animal(); // 创建Animal类对象 12 System.out.println(animal.toString()); // 调用toString()方法并打印 //是一些看不懂的字符串 13 } 14}
在实际开发中,通常情况下不会直接调用Object类中的方法,因为Object类中的方法并不能适用于所有的子类,这时就需要对Object类中的方法进行重写,以符合实际开发需求。
----------------------------------------------------------------------------------
1// 定义Animal类 2class Animal { 3 //重写Object类的toString()方法 4 public String toString(){ 5 return "这是一个动物。"; 6 } 7}
这样就不会出现奇怪的字符串了。
clone()方法,生成对象副本,默认是浅拷贝,如果深拷贝要手动重写。
内部类
成员内部类
在一个类中除了可以定义成员变量、成员方法,还可以定义类,这样的类被称作成员内部类。成员内部类可以访问外部类的所有成员,无论外部类的成员是何种访问权限。如果想通过外部类访问内部类,则需要通过外部类创建内部类对象
外部类名 外部类对象 = new 外部类名(); 外部类名.内部类名 内部类对象 = 外部类对象.new 内部类名();
1class Outer { 2 int m = 0; // 定义类的成员变量 3 //外部类方法test1() 4 void test1() { 5 System.out.println("外部类成员方法test1()"); 6 } 7 // 下面的代码定义了一个成员内部类Inner 8 class Inner { 9 int n = 1; 10 void show1() { 11 // 在成员内部类的方法中访问外部类的成员变量m 12 System.out.println("外部成员变量m = " + m); 13 // 在成员内部类的方法中访问外部类的成员方法test1() 14 test1(); 15 } 16 void show2() { 17 System.out.println("内部成员方法show2()"); 18 } 19 } 20 //外部类方法test2() 21 void test2() { 22 Inner inner = new Inner(); //实例化内部类对象inner 23 System.out.println("内部成员变量n = " + inner.n); //访问内部类变量和方法 24 inner.show2(); 25 } 26} 27public class Example20 { 28 public static void main(String[] args) { 29 Outer outer = new Outer(); //实例化外部类对象outer 30 Outer.Inner inner = outer.new Inner(); //实例化内部类对象inner 31 inner.show1(); //在内部类中访问外部类的成员变量m和成员方法test1() 32 outer.test2(); //在外部类中访问内部类的成员变量n和成员方法show2() 33 } 34}
在Java中,当内部类和外部类存在同名成员变量时,会发生变量遮蔽(Shadowing),此时默认直接访问的是内部类的变量。如果希望访问外部类的同名变量,需要通过外部类名.this语法显式指明所属作用域。
局部内部类
局部内部类,也称为方法内部类,是指定义在某个局部范围中的类,它和局部变量都是在方法中定义的,有效范围只限于方法内部。 在局部内部类中,局部内部类可以访问外部类的所有成员变量和成员方法,而在外部类中无法直接访问局部内部类中的变量和方法。如果要在外部类中访问局部内部类的成员,只能在局部内部类的所属方法中创建局部内部类的对象,通过对象访问局部内部类的变量和方法。
1class Outer { 2 int m = 0; // 定义类的成员变量 3 //定义一个成员方法test1() 4 void test1() { 5 System.out.println("外部类成员方法test1()"); 6 } 7 void test2() { 8 //定义一个局部内部类,在局部内部类中访问外部类变量和方法 9 class Inner { 10 int n = 1; 11 void show() { 12 System.out.println("外部成员变量m = " + m); 13 test1(); 14 } 15 } 16 //访问局部内部类中的变量和方法 17 Inner inner = new Inner(); 18 System.out.println("局部内部类变量n = " + inner.n); 19 inner.show(); 20 } 21} 22public class Example21 { 23 public static void main(String[] args) { 24 Outer outer = new Outer(); 25 outer.test2(); //通过外部类对象outer调用创建了局部内部类的方法test2() 26 } 27}
静态内部类
静态内部类,就是使用static关键字修饰的成员内部类。与成员内部类相比,在形式上,静态内部类只是在内部类前增加了static关键字,但在功能上,静态内部类只能访问外部类的静态成员,通过外部类访问静态内部类成员时,因为程序已经提前在静态常量区分配好了内存,所以即使静态内部类没有加载,依然可以通过外部类直接创建一个静态内部类对象。
外部类名.静态内部类名 变量名 = new 外部类名.静态内部类名();
1class Outer { 2 static int m = 0; // 定义类的静态变量 3 // 下面的代码定义了一个静态内部类 4 static class Inner { 5 int n = 1; 6 void show() { 7 // 在静态内部类的方法中访问外部类的静态变量m 8 System.out.println("外部类静态变量m = " + m); 9 } 10 } 11 } 12public class Example22 { 13 public static void main(String[] args) { 14 Outer.Inner inner = new Outer.Inner(); 15 inner.show(); 16 } 17}
匿名内部类
在Java中调用某个方法时,如果该方法的参数是接口类型,那么在传参时,除了可以传入一个接口实现类,还可以传入实现接口的匿名内部类作为参数,在匿名内部类中实现接口方法。匿名内部类就是没有名称的内部类,定义匿名内部类时,其类体作为new语句的一部分。
new 继承的父类或实现的接口名(){ //匿名内部类的类体 }
1interface Animal{ //定义接口Animal 2 void shout(); //定义抽象方法shout() 3} 4public class Example23{ 5 public static void main(String[] args){ 6 String name = "小花"; 7 animalShout(new Animal(){//调用animalShout()方法,参数为匿名内部类 8 @Override 9 public void shout() { 10 System.out.println(name+"喵喵..."); 11 } 12 }); 13 } 14 public static void animalShout(Animal an){ //该方法参数为Animal接口类型 15 an.shout(); 16 } 17}
注意:在JDK 8之前,局部变量前必须加final修饰符,否则程序编译时报错。在案例中的匿名内部类中访问了局部变量name,而局部变量name并没有使用final修饰符修饰,程序也没有报错。这是因为JDK 8的新增特性,允许在局部内部类、匿名内部类中访问非final修饰的局部变量。
匿名对象
匿名类要么继承一个类,要么实现一个接口,不能同时做这两件事。
匿名类不能有显式定义的构造函数,因为它们没有类名。但可以有实例初始化块来模拟构造函数行为。
匿名类的定义和使用是一次性的,不能像普通类那样重复使用定义来创建多个实例。
每次创建匿名类实例时,编译器都会生成一个独特的类名(如OuterClass1,OuterClass2等)。每个匿名对象都具有独一无二的类型
6.异常
什么是异常
Java中的异常是指Java程序在运行时可能出现的错误或非正常情况,比如在程序中试图打开一个根本不存在的文件,在程序中除0等。异常是否出现,通常取决于程序的输入、程序中对象的当前状态以及程序所处的运行环境。程序抛出异常之后,会对异常进行处理。异常处理将会改变程序的控制流程,出于安全性考虑,同时避免异常程序影响到其他正常程序的运行,操作系统通常将出现异常的程序强行中止,并弹出系统错误提示。
举例:
package com.itheima; public class Example01 { public static void main(String[] args) { int result = divide(4, 0); // 调用divide()方法,第2个参数为0 System.out.println(result); } //下面的方法实现了两个整数相除 public static int divide(int x, int y) { int result = x / y; // 定义一个变量result记录两个数相除的结果 return result; // 将结果返回 } }
程序发生了算术异常(ArithmeticException),提示运算时出现了被0除的情况。异常发生后,程序会立即结束,无法继续向下执行。
-----------
Java提供了大量的异常类,每一个异常类都表示一种预定义的异常,这些 异常类都继承自java.lang包下的Throwable类。
Throwable类是所有异常类的父类,它有两个直接子类Error类和Exception类,其中,Error类代表程序中产生的错误,Exception类代表程序中产生的异常。
Error类称为错误类,它表示Java程序运行时产生的系统内部错误或资源耗尽的错误, 这类错误比较严重,仅靠修改程序本身是不能恢复执行的。 例如,使用java命令去运行一个不存在的类就会出现Error错误。 Exception类称为异常类,它表示程序本身可以处理的错误,在Java程序中进行的异常处理, 都是针对Exception类及其子类的。 在Exception类的众多子类中有一个特殊的子类——RuntimeException类, RuntimeException类及其子类用于表示运行时异常。 Exception类的其他子类都用于表示编译时异常。
Throwable类的常用方法:
运行时异常与编译时异常
编译时异常
在实际开发中,经常会在程序编译时产生异常,这些异常必须要进行处理,否则程序无法正常运行,这种异常被称为编译时异常,也称为checked异常。在Exception类中,除了RuntimeException类及其子类,Exception的其他子类都是编译时异常。编译时异常的特点是Java编译器会对异常进行检查,如果出现异常就必须对异常进行处理,否则程序无法通过编译。
处理编译时期的异常有两种方式,具体如下: (1)使用try…catch语句对异常进行捕获处理。 (2)使用throws关键字声明抛出异常,调用者对异常进行处理。
运行时异常
另外还有一种异常是在程序运行时产生的,这种异常即使不编写异常处理代码,依然可以通过编译,因此被称为运行时异常,也称为unchecked异常。RuntimeException类及其子类都是运行时异常。运行时异常的特点是在程序运行时由Java虚拟机自动进行捕获处理的,Java编译器不会对异常进行检查。也就是说,当程序中出现这类异常时,即使没有使用try…catch语句捕获或使用throws关键字声明抛出,程序也能编译通过,只是程序在运行过程中可能报错。
运行时异常一般是由程序中的逻辑错误引起的,在程序运行时无法恢复。例如,通过数组的索引访问数组的元素时,如果索引超过了数组范围,就会发生索引越界异常
int[] arr=new int[5]; System.out.println(arr[6]);
在上面的代码中,由于数组arr的length为5,最大索引应为4,当使用arr[6]访问数组中的元素就会发生数组索引越界的异常。
异常处理及语法
try...catch语句
try{ 代码块 }catch(ExceptionType e){ 代码块 }
(1)try代码块是必需的。 (2)catch代码块和finally代码块都是可选的, 但catch代码块和finally代码块至少要出现一个。 (3)catch代码块可以有多个,但捕获父类异常的catch代码块必须 位于捕获子类异常的catch代码块后面。 (4)catch代码块必须位于try代码块之后。
异常处理完毕,最后执行其他程序语句。
举例:
public class Example02 { public static void main(String[] args) { try { int result = divide(4, 0); //调用divide()方法 System.out.println(result); } catch (Exception e) { //对异常进行处理 System.out.println("捕获的异常信息为:" + e.getMessage()); } System.out.println("程序继续向下执行..."); } //下面的方法实现了两个整数相除 public static int divide(int x, int y) { int result = x / y; //定义一个变量result记录两个数相除的结果 return result; //将结果返回 } }
注意:在try代码块中,发生异常语句后面的代码是不会被执行的
finally语句
try{ 代码块 } catch(ExceptionType e){ 代码块 } finally{ 代码块 } 注意:finally代码块必须位于所有catch代码块之后。
需要注意的是,如果程序发生异常但是没有被捕获到,在执行完finally代码块中的代码之后,程序会中断执行。
public static void main(String[] args) { //下面的代码定义了一个try…catch…finally语句用于捕获异常 try { int result = divide(4, 0); //调用divide()方法 System.out.println(result); } catch (Exception e) { //对捕获到的异常进行处理 System.out.println("捕获的异常信息为:" + e.getMessage()); return; //用于结束当前语句 } finally { System.out.println("进入finally代码块"); } System.out.println("程序继续向下…"); } //下面的方法实现了两个整数相除 public static int divide(int x, int y) { int result = x / y; //定义一个变量result记录两个数相除的结果 return result; //将结果返回 }
注意:如果在try...catch中执行了System.exit(0)语句,finally代码块不再执行。 System.exit(0)表示退出当前的Java虚拟机,Java虚拟机停止了,任何代码都不能再执行了。
抛出异常
throws关键字
在实际开发中,大部分情况下我们会调用别人编写的方法,并不知道别人编写的方法是否会发生异常。针对这种情况,Java允许在方法的后面使用throws关键字声明该方法有可能发生的异常,这样调用者在调用方法时,就明确地知道该方法有异常,并且必须在程序中对异常进行处理,否则编译无法通过。
修饰符 返回值类型 方法名(参数1,参数2,…)throws 异常类1, 异常类2...{ 方法体 }
throws关键字需要写在方法声明的后面,throws后面还需要声明方法中发生异常的类型。
举例:
public class Example04 { public static void main(String[] args) { int result = divide(4, 2); //调用divide()方法 System.out.println(result); } //下面的方法实现了两个整数相除,并使用throws关键字声明抛出异常 public static int divide(int x, int y) throws Exception { int result = x / y; //定义一个变量result记录两个数相除的结果 return result; //将结果返回 } }
第3行代码调用divide()方法时传入的第2个参数为2,程序在运行时不会发生被0除的异常。但是运行程序依然会提示错误,这是因为定义divide()方法时使用throws关键字声明了该方法可能抛出的异常,调用者必须在调用divide()方法时对抛出的异常进行处理,否则就会发生编译错误。
因此要这样
public class Example05 { public static void main(String[] args) { //下面的代码定义了一个try…catch语句用于捕获异常 try { int result = divide(4, 2); //调用divide()方法 System.out.println(result); } catch (Exception e) { //对捕获到的异常进行处理 e.printStackTrace(); //打印捕获的异常信息 } } //下面的方法实现了两个整数相除,并使用throws关键字声明抛出异常 public static int divide(int x, int y) throws Exception { int result = x / y; //定义一个变量result记录两个数相除的结果 return result; //将结果返回 } }
注意:使用throws关键字重抛异常时,如果程序发生了异常,并且上一层调用者也无法 处理异常时,那么异常会继续被向上抛出,最终直到系统接收到异常,终止程序执行。
public class Example06 { public static void main(String[] args)throws Exception { int result = divide(4, 0); // 调用divide()方法 System.out.println(result); } // 下面的方法实现了两个整数相除,并使用throws关键字声明抛出异常 public static int divide(int x, int y) throws Exception { int result = x / y; // 定义一个变量result记录两个数相除的结果 return result; // 将结果返回 } }
main()方法继续使用throws关键字将Exception抛出,程序虽然可以通过编译,但从上图的运行结果可以看出,在运行时期由于没有对“/by zero”的异常进行处理,最终导致程序终止运行。
throw关键字
在Java程序中,除了throws关键字,还可以使用throw关键字抛出异常。与throws关键字不同的是,throw关键字用于方法体内,抛出的是一个异常实例,并且每次只能抛出一个异常实例。
throw ExceptionInstance; 在方法中,通过throw关键字抛出异常后, 还需要使用throws关键字或try…catch对异常进行处理。 如果throw抛出的是error、RuntimeException或它们的子类异常对象, 则无需使用throws关键字或try…catch对异常进行处理。
(1)当throw关键字抛出的异常是编译时异常时, 第一种处理方式是在try代码块里使用throw关键字抛出异常,通过try代码块捕获该异常; 第二种处理方式是在一个有throws声明的方法中使用throw关键字抛出异常, 把异常交给该方法的调用者处理。 (2)当throw关键字抛出的异常是运行时异常时, 程序既可以显式使用try…catch捕获并处理该异常,也可以完全不理会该异常, 而把该异常交给方法的调用者处理。
// 定义printAge()输出年龄 public static void printAge(int age) throws Exception { if(age <= 0){ // 对业务逻辑进行判断,当输入年龄为负数时抛出异常 throw new Exception("输入的年龄有误,必须是正整数!"); }else { System.out.println("此人年龄为:"+age); } } public static void main(String[] args) { // 下面的代码定义了一个try…catch语句用于捕获异常 int age = -1; try { printAge(age); } catch (Exception e) { // 对捕获到的异常进行处理 System.out.println("捕获的异常信息为:" + e.getMessage()); } }
自定义异常类
Java中定义了大量的异常类,虽然这些异常类可以描述编程时出现的大部分异常情况,但是在程序开发中有时可能需要描述程序中特有的异常情况。例如,两数相除,不允许被除数为负数。此时,就无法使用Java提供的异常类表示该类异常,为了解决这个问题,Java允许用户自定义异常类,自定义的异常类必须继承自Exception或其子类。
public class DivideByMinusException extends Exception{ public DivideByMinusException (){ super(); // 调用Exception无参的构造方法 } public DivideByMinusException (String message){ super(message); // 调用Exception有参的构造方法 } }
在实际开发中,如果没有特殊的要求,自定义的异常类只需继承Exception类,在构造方法 中使用super()语句调用Exception的构造方法即可。
使用自定义的异常类,需要用到throw关键字。
throw Exception 异常对象
举例:
public class DivideByMinusException extends Exception{ public DivideByMinusException (){ super(); // 调用Exception无参的构造方法 } public DivideByMinusException (String message){ super(message); // 调用Exception有参的构造方法 } } public class Example08 { public static void main(String[] args) { int result = divide(4, -2); System.out.println(result); } //下面的方法实现了两个整数相除 public static int divide(int x, int y) { if(y<0){ throw new DivideByMinusException("除数是负数"); } int result = x / y; // 定义一个变量result记录两个数相除的结果 return result; // 将结果返回 } }
这样的话会报错,提示没有捕获异常
所以可以这样
public class DivideByMinusException extends Exception{ public DivideByMinusException (){ super(); // 调用Exception无参的构造方法 } public DivideByMinusException (String message){ super(message); // 调用Exception有参的构造方法 } } // 下面的方法实现了两个整数相除,并使用throws关键字声明抛出自定义异常 public static int divide(int x, int y) throws DivideByMinusException{ if (y < 0) { throw new DivideByMinusException("除数是负数"); } int result = x / y; // 定义一个变量result记录两个数相除的结果 return result; // 将结果返回 } } public class Example09 { public static void main(String[] args) { // 下面的代码定义了一个try…catch语句用于捕获异常 try { int result = divide(4, -2); System.out.println(result); } catch (DivideByMinusException e) {// 对捕获到的异常进行处理 System.out.println(e.getMessage()); // 打印捕获的异常信息 } } }
7.常用类
API(Application Programming Interface)指的是应用程序编程接口,API可以让编程变得更加方便简单。Java也提供了大量API供程序开发者使用,即Java API。Java API指的就是JDK提供的各种功能的Java类库,如Arrays、Collection,Math类等,都是Java提供给开发者的类库。
字符串类
String类
初始化
1. 使用字符串常量直接初始化一个String对象
String 变量名 = 字符串; String str1 = null; //将字符串str1设置为空 String str2 = ""; //将字符串str2设置为空字符串 String str3 = "abc"; //将字符串str3设置为abc
每个字符串常量都可以当作一个String类的对象使用,因此字符串常量可以直接调用String类中提供的API
int len = "Hello World".length(); //len为11,即字符串包含字符的个数
String类是专门用于处理字符串的类。字符串一旦被创建,其内容就不能再改变。
String s = "hello"; s = "helloworld"; 不要被迷惑了。这里改变的只是s指向的字符串常量罢了。 字符串常量本身没有被改变。原来是指向一个hello的常量。后面新增了一个独立的helloworld常量, s指向了这个常量。但hello还是存在的,自己本身还是没有变的。
2. 调用String类的构造方法初始化字符串对象
String 变量名 = new String(字符串); 在上述语法中,字符串同样可以为空或是一个具体的字符串。 当为具体字符串时,String会根据参数类型调用相应的构造方法来初始化字符串对象。
public class Example01 { public static void main(String[] args) throws Exception { // 创建一个空的字符串 String str1 = new String(); // 创建一个内容为abcd的字符串 String str2 = new String("abcd"); // 创建一个字符数组 char[] charArray = new char[] { 'D', 'E', 'F' }; String str3 = new String(charArray); // 创建一个字节数组 byte[] arr = {97,98,99}; String str4 = new String(arr); System.out.println("a" + str1 + "b"); System.out.println(str2); System.out.println(str3); System.out.println(str4); } }
内置函数
举例:
1.获取字符串长度以及访问字符串中的字符 public class Example02 { public static void main(String[] args) { String s = "ababcdedcba"; // 定义字符串s // 获取字符串长度,即字符个数 System.out.println("字符串的长度为:" + s.length()); System.out.println("字符串中第一个字符:" + s.charAt(0)); System.out.println("字符c第一次出现的位置:" + s.indexOf('c')); System.out.println("字符c最后一次出现的位置:" + s.lastIndexOf('c')); System.out.println("子字符串ab第一次出现的位置:" + s.indexOf("ab")); System.out.println("子字符串ab字符串最后一次出现的位置:" + s.lastIndexOf("ab")); } }
2.字符串的转换操作 public static void main(String[] args) { String str = "abcd"; System.out.print("将字符串转为字符数组后的结果:"); char[] charArray = str.toCharArray(); // 字符串转换为字符数组 for (int i = 0; i < charArray.length; i++) { if (i != charArray.length - 1) { // 如果不是数组的最后一个元素,在元素后面加逗号 System.out.print(charArray[i] + ","); } else { //如果不是数组的最后一个元素,则在元素后不加逗号 System.out.println(charArray[i]); } } System.out.println("将int值转换为String类型之后的结果:String.valueOf(12)); System.out.println("将字符串转换成大写之后的结果:str.toUpperCase()); System.out.println("将字符串转换成小写之后的结果:str.toLowerCase()); }
3.字符串的替换和去除空格操作 public class Example04 { public static void main(String[] args) { String s = "itcast"; // 字符串替换操作 System.out.println("将it替换成cn.it的结果:" + s.replace("it", "cn.it")); // 字符串去除空格操作 String s1 = " i t c a s t "; System.out.println("去除字符串两端空格后的结果:" + s1.trim()); System.out.println("去除字符串中所有空格后的结果:" + s1.replace(" ", "")); } }
4.字符串判断 public class Example05 { public static void main(String[] args) { String s1 = "String"; // 定义一个字符串 String s2 = "string"; System.out.println("判断s1字符串对象是否以Str开头:" + s1.startsWith("Str")); System.out.println("判断是否以字符串ng结尾:" + s1.endsWith("ng")); System.out.println("判断是否包含字符串tri:" + s1.contains("tri")); System.out.println("判断字符串是否为空:" + s1.isEmpty()); System.out.println("判断s1和s2内容是否相同:" + s1.equals(s2)); System.out.println("忽略大小写的情况下判断s1和s2内容是否相同:" + s1.equalsIgnoreCase(s2)); System.out.println("按对应字符的Unicode比较s1和s2的大小:" + s1.compareTo(s2)); } }
5.字符串的截取和分割操作 public static void main(String[] args) { String str = "石家庄-武汉-哈尔滨"; // 下面是字符串截取操作 System.out.println("从第5个字符截取到末尾的结果:str.substring(4)); System.out.println("从第5个字符截取到第6个字符的结果:str.substring(4, 6)); // 下面是字符串分割操作 System.out.print("分割后的字符串数组中的元素依次为:"); String[] strArray = str.split("-"); // 将字符串转换为字符串数组 for (int i = 0; i < strArray.length; i++) { if (i != strArray.length - 1) { // 如果不是数组的最后一个元素,在元素后面加逗号 System.out.print(strArray[i] + ","); } else { System.out.println(strArray[i]);// 数组的最后一个元素后面不加逗号 } } }
特殊用法
字符串连接运算符
连接字符串可以通过运算符“+”来实现,("a" + str1+ "b"),“+”的作用就是将两个字符串拼接到一起并生成一个新的字符串。在Java程序中,如果“+”的两边操作数中有一个为String类型,那么“+”就表示字符串连接运算符。
---------------
在判断两个字符串是否相等时,可以通过“==”和equals()方法两种方式对字符串进行比较,但这两种方式有明显的区别。equals()方法用于比较两个字符串内容是否相等,==方法用于比较两个字符串对象的地址是否相同。对于两个内容完全一样的字符串对象,调用equals()方法判断的结果是true,使用==判断的结果是false。为了便于理解,下面给出示例代码。
String str1 = new String("abc"); String str2 = new String("abc"); /*使用==判断的结果为false,因为 *str1和str2是两个对象,地址不同*/ System.out.println(str1 == str2); /*使用equals判断的结果为true, *因为str1和str2字符内容相同*/ System.out.println(str1.equals(str2));
------------------------
String字符串在获取某个字符时,会用到字符的索引,当访问字符串中的字符时,如果字符的索引不存在,则会发生StringIndexOutOfBoundsException(字符串索引越界异常)。
StringBuffer类
在Java中,因为String类是final类型的,所以使用String定义的字符串是一个常量,也就是说使用String定义的字符串一旦创建,其内容和长度是不可改变的。为了便于对字符串进行修改,Java提供了StringBuffer类(也称字符串缓冲区)来操作字符串。StringBuffer类和String类最大的区别在于它的内容和长度都是可以改变的。StringBuffer类似一个字符容器,当在其中添加或删除字符时,所操作的都是这个字符容器,因此并不会产生新的StringBuffer对象。
public static void add() { StringBuffer sb = new StringBuffer(); // 定义一个字符串缓冲区 sb.append("abcdefg"); // 在末尾添加字符串 sb.append("hij").append("klmn"); // 连续调用append()方法添加字符串 System.out.println("append添加结果:" + sb); sb.insert(2, "123"); // 在指定位置插入字符串 System.out.println("insert添加结果:" + sb); } public static void remove() { StringBuffer sb = new StringBuffer("abcdefg"); sb.delete(1, 5); // 指定范围删除 System.out.println("删除指定位置结果:" + sb); sb.deleteCharAt(2); // 指定位置删除 System.out.println("删除指定位置结果:" + sb); sb.delete(0, sb.length()); // 清空缓冲区 System.out.println("清空缓冲区结果:" + sb); } public static void alter() { StringBuffer sb = new StringBuffer("abcdef"); sb.setCharAt(1, 'p'); // 修改指定位置字符 System.out.println("修改指定位置字符结果:" + sb); sb.replace(1, 3, "qq"); // 替换指定位置字符串或字符 System.out.println("替换指定位置字符(串)结果:" + sb); System.out.println("字符串翻转结果:" + sb.reverse()); } public static void sub() { StringBuffer sb = new StringBuffer(); // 定义一个字符串缓冲区 System.out.println("获取sb的初始容量:" + sb.capacity()); sb.append("itcast123"); // 在末尾添加字符串 System.out.println("append添加结果:" + sb); System.out.println("截取第7~9个字符:" + sb.substring(6,9));} }
StringBuilder类
除了StringBuffer类,还可以使用StringBuilder类修改字符串,StringBuffer类和StringBuilder类的对象都可以被多次修改,且不产生新的未使用对象。StringBuilder类与StringBuffer类的功能相似,且两个类中所提供的方法也基本相同。二者之间最大不同在于StringBuffer的方法是线程安全的,而StringBuilder没有实现线程安全功能,所以性能略高。通常情况下,如果创建一个内容可变的字符串对象,应该优先考虑使用StringBuilder类。
-------------
StringBuilder同样提供了一系列添加(append)、插入(insert)、替换(raplace)和删除(delete)的方法,读者可以参考StringBuffer类的常用方法表学习StringBuilder常见操作。 StringBuilder类、StringBuffer类和String类有很多相似之处,初学者在使用时很容易混淆。接下来针对这三个类进行对比,简单归纳一下三者的不同,具体如下。
(1)String类表示的字符串是常量,一旦创建后,内容和长度都是无法改变的。而StringBuilder和StringBuffer表示字符容器,其内容和长度可以随时修改。在操作字符串时,如果该字符串仅用于表示数据类型,则使用String类即可,但是如果需要对字符串中的字符进行增删操作,则使用StringBuffer与StringBuilder类。如果有大量字符串拼接操作,并且不要求线程安全的情况下,采用StringBuilder类更高效。相反如果需要线程安全则需要使用StringBuffer类。
(2)对于euals()方法的使用我们已经有所了解,但是StringBuffer类与StringBuilder类中并没有重写Object类的equals()方法,也就是说,equals()方法对于StringBuffer类与StringBuilder类来言并不起作用
(3)String类对象可以用操作符“+”进行连接,而StringBuffer类和StringBuild类的对象之间不能
System类
System类定义了一些与系统相关的属性和方法,它所提供的属性和方法都是静态的,因此,可以使用System类直接引用类中的属性和方法。
1.arraycopy()方法 static void arraycopy(Object src,int srcPos,Object dest, int destPos,int length) src:表示源数组。 dest:表示目标数组。 srcPos:表示源数组中复制元素的起始位置,即从哪个位置开始复制元素。 destPos:表示复制到目标数组的起始位置,即复制到目标数组的哪个位置。 length:表示复制元素的个数。 注意:在进行数组元素复制时,目标数组必须有足够的空间来存放复制的元素,否则会发生索引越界异常。 public class Example09 { public static void main(String[] args) { int[] fromArray = { 10, 11, 12, 13, 14, 15 }; // 源数组 int[] toArray = { 20, 21, 22, 23, 24, 25, 26 }; // 目标数组 System.arraycopy(fromArray, 2, toArray, 3, 4); // 复制数组元素 // 打印复制后数组的元素 System.out.println("复制后的数组元素为:"); for (int i = 0; i < toArray.length; i++) { System.out.print(toArray[i]+" "); } } } 结果:20 21 22 12 13 14 15
2.currentTimeMillis()方法 currentTimeMillis()方法用于获取当前系统的时间,返回值类型是long, 该值表示当前时间与1970年1月1日0点0分0秒之间的时间差,单位是毫秒, 通常也将该值称作时间戳(系统当前时间)。 public class Example10 { public static void main(String[] args) { long startTime = System.currentTimeMillis();// 循环开始时的当前时间 int sum = 0; for (int i = 0; i < 1000000000; i++) { sum += i; } long endTime = System.currentTimeMillis();// 循环结束后的当前时间 System.out.println("程序运行的时间为:"+(endTime - startTime)+"毫秒"); } }
3.getProperties()和getProperty()方法 System类的getProperties()方法用于获取当前系统的全部属性, 该方法会返回一个Properties对象,Properties对象封装了系统的所有属性, 这些属性以键值对形式存在。getProperty()方法可以根据系统的属性名获取对应的属性值。 public class Example11 { public static void main(String[] args) { // 获取当前系统属性 Properties properties = System.getProperties(); // 获取所有系统属性的key,返回Enumeration对象 Enumeration propertyNames = properties.propertyNames(); while (propertyNames.hasMoreElements()) { // 获取系统属性的键key String key = (String) propertyNames.nextElement(); // 获取当前键key对应的值value String value = System.getProperty(key); System.out.println(key + "--->" + value); } } }
4.gc()方法 在Java中,一个对象如果不再被任何栈内存所引用,该对象就称为垃圾对象。 垃圾对象会占用内存空间,时间一长,垃圾对象越来越多,就会导致内存空间不足。 针对这种情况,Java引入了垃圾回收机制。除了等待Java虚拟机进行自动垃圾回收外, 还可以通过调用System.gc()方法通知Java虚拟机立即进行垃圾回收。 class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "姓名:"+this.name+",年龄:"+this.age; } // 下面定义的finalize方法会在垃圾回收前被调用 public void finalize() throws Throwable { System.out.println("对象被释放-->"+this); } //这个finalize跟c++的析构函数差不多。 } public class Example12{ public static void main(String[] args) { // 下面是创建Person对象 Person p = new Person("张三",20); // 下面将变量置为null,让对象p成为垃圾 p = null; // 调用方法进行垃圾回收 System.gc(); for (int i = 0; i < 1000000; i++) { // 为了延长程序运行的时间,执行空循环 } } }
Runtime类
Runtime类用于封装JVM虚拟机进程,通过Runtime类,可以获取虚拟机运行时状态。每一个JVM都对应着一个Runtime类的实例。在JDK文档中读者不会发现任何有关Runtime类构造方法的定义,这是因为Runtime类本身的构造方法是私有化的(单例设计模式),若想在程序中获取一个Runtime类实例,只能通过调用getRuntime()方法获取,getRuntime()方法是Runtime类提供的一个静态方法,用于获取Runtime类实例。
getRuntime()方法 由于Runtime类封装了虚拟机进程,因此, 在程序中通常会通过Runtime类的实例对象获取当前虚拟机的相关信息。 通过调用getRuntime()方法获取Runtime类实例的具体方式如下。 Runtime run = Runtime.getRuntime();
1.获取当前虚拟机信息 Runtime类可以获取当前Java虚拟机的处理器的个数、空闲内存量、最大可用内存量和内存总量的信息等, 通过这些信息可以清楚地知道JVM的内存使用情况。 public class Example13 { public static void main(String[] args) { Runtime rt = Runtime.getRuntime(); // 创建Runtime对象 System.out.println("处理器的个数: " + rt.availableProcessors()+"个"); System.out.println("空闲内存数量: " + rt.freeMemory() / 1024 / 1024 + "M"); System.out.println("最大可用内存数量: " + rt.maxMemory() / 1024 / 1024 + "M"); System.out.println("虚拟机中内存总量: " + rt.totalMemory() / 1024 / 1024 + "M"); } } 空闲内存量、可用最大内存量和内存总量都是以字节为单位计算的
2.操作系统进程 Runtime类中提供了一个exec()方法,该方法用于执行一个DOS命令, exec()方法的执行效果与DOS命令的效果相同。 import java.io.IOException; public class Example14{ public static void main(String[] args) throws IOException { Runtime rt = Runtime.getRuntime(); // 创建Runtime实例对象 rt.exec("notepad.exe"); // 调用exec()方法 } } Runtime类的exec()方法的返回值为Process类型的对象, 表示一个操作系统的进程类,通过Process类可以进行系统进程的控制 public class Example { public static void main(String[] args) throws Exception { Runtime rt = Runtime.getRuntime(); // 创建一个Runtime实例对象 Process process = rt.exec("notepad.exe");//得到表示进程的Process对象 Thread.sleep(3000); // 程序休眠3秒 process.destroy(); //关闭进程 } }
Math类
Math类是一个工具类,类中包含许多用于进行科学计算的方法,如计算一个数的平方根、绝对值或获取一个随机数等。因为Math类构造方法的访问权限是private,所以无法创建Math类的对象。Math类中所有方法都是静态方法,可以直接通过类名调用Math类中的方法。除静态方法外,Math类中还定义了两个静态常量PI和E,分别代表数学中的π和e。
public class Example15 { public static void main(String[] args) { System.out.println("计算-10的绝对值: " + Math.abs(-10)); System.out.println("求大于5.6的最小整数: " + Math.ceil(5.6)); System.out.println("求小于-4.2的最大整数: " + Math.floor(-4.2)); System.out.println("对-4.6进行四舍五入: " + Math.round(-4.6)); System.out.println("求2.1和-2.1中的较大值: " + Math.max(2.1, -2.1)); System.out.println("求2.1和-2.1中的较小值: " + Math.min(2.1, -2.1)); System.out.println("生成一个大于等于0.0小于1.0随机值: " +Math.random()); System.out.println("计算1.57的正弦结果: "+Math.sin(1.57)); System.out.println("计算4的开平方的结果: "+Math.sqrt(4)); } }
Random类
Random类可以产生指定取值范围的随机数字。
构造:
import java.util.Random; public class Example16 { public static void main(String args[]) { Random random = new Random(); // 不传入种子 // 随机产生10个[0,100)之间的整数 for (int x = 0; x < 10; x++) { System.out.println(random.nextInt(100)); } } } 这是因为创建Random的对象时,没有指定种子,系统会以当前时间戳作为种子,产生随机数。 由于每一时刻的时间戳都不一样,所以每一次运行时,产生的随机数也不一样。
import java.util.Random; public class Example17 { public static void main(String args[]) { Random r = new Random(13); // 创建对象时传入种子 // 随机产生10个[0,100)之间的整数 for (int x = 0; x < 10; x++) { System.out.println(r.nextInt(100)); } } }
常用方法:
import java.util.Random; public class Example18 { public static void main(String[] args) { Random r = new Random(); // 创建Random实例对象 System.out.println("生成boolean类型的随机数: " + r.nextBoolean()); System.out.println("生成float类型的随机数: " + r.nextFloat()); System.out.println("生成double类型的随机数:" + r.nextDouble()); System.out.println("生成int类型的随机数:" + r.nextInt()); System.out.println("生成0~100之间int类型的随机数:" +r.nextInt(100)); System.out.println("生成long类型的随机数:" + r.nextLong()); } }
BigInteger类
当程序需要处理一个非常大的整数时,如果这个数值超出了long类型的取值范围,则无法使用基本类型接收。早期程序开发者使用String类进行大整数的接收,使用String类接收大整数之后,再采用拆分的方式进行计算,操作过程非常麻烦。为了解决这个问题,Java提供了BigInteger类。BigInteger表示大整数类,定义在java.math包中,如果在开发时需要定义一个超出long类型的整型数据,可以使用BigInteger类的对象接收该数据。
常用方法
import java.math.BigInteger; class Example19 { public static void main(String[] args) { BigInteger bi1 = new BigInteger("123456789"); // 创建BigInteger对象 BigInteger bi2 = new BigInteger("987654321"); // 创建BigInteger对象 System.out.println("bi2与bi1的和: " + bi2.add(bi1)); System.out.println("bi2与bi1的差: " + bi2.subtract(bi1)); System.out.println("bi2与bi1的积: " + bi2.multiply(bi1)); System.out.println("bi2与bi1的商: " + bi2.divide(bi1)); System.out.println("bi2与bi1之间的较大值: " + bi2.max(bi1)); System.out.println("bi2与bi1之间的较小值: " + bi2.min(bi1)); //创建BigInteger数组接收bi2除以bi1的商和余数 BigInteger result[] = bi2.divideAndRemainder(bi1); System.out.println("bi2除以bi1的商: " + result[0]+":bi2除以bi1的余数:"+result[1]); } }
BigDecimal类
在进行浮点数运算的时候,float类型和double类型很容易丢失精度,为了能够精确的表示、计算浮点数,Java提供了BigDecimal类。BigDecimal类可以表示任意精度的小数,多用于数字精度要求高的场景,例如商业计算、货币值计算等。
常用方法:
import java.math.BigDecimal; public class Example20 { public static void main(String[] args) { BigDecimal bd1 = new BigDecimal("0.001"); // 创建BigDecimal对象 BigDecimal bd2 = BigDecimal.valueOf(0.009);// 创建BigDecimal对象 System.out.println("bd2与bd1的和: " + bd2.add(bd1)); System.out.println("bd2与bd1的差: " + bd2.subtract(bd1)); System.out.println("bd2与bd1的积: " + bd2.multiply(bd1)); System.out.println("bd2与bd1的商: " + bd2.divide(bd1)); System.out.println("bd2与bd1之间的较大值: " + bd2.max(bd1)); System.out.println("bd2与bd1之间的较小值: " + bd2.min(bd1)); } }
日期与时间类
JDK的java.util包提供了一个Date类用于表示日期和时间,Date类在JDK 1.0时就已经开始使用。随着JDK版本的不断升级和发展,Date类中大部分的构造方法和普通方法都已经不再推荐使用。在JDK 11中,Date类只有下面两个构造方法是实际开发中经常被应用到的。
Date():用于创建当前日期时间的Date对象。 Date(long date):用于创建指定时间的Date对象, 其中date参数表示1970年1月1日0时0分0(称为历元)以来的毫秒数,即时间戳。
import java.util.*; public class Example19 { public static void main(String[] args) { // 创建表示当前时间的Date对象 Date date1 = new Date(); // 获取当前时间后1秒的时间 Date date2 = new Date(System.currentTimeMillis() + 1000); System.out.println(date1); System.out.println(date2); } }
Calendar类
所以从 JDK 1.1 开始,Java提供了Calendar类,用Calendar类中的方法取代了Date类的相应功能。Calendar类也用于完成日期和时间字段的操作,它可以通过特定的方法设置和读取日期的特定部分,比如年、月、日、时、分、秒等。
常用方法:
getInstance()方法
Calendar类是一个抽象类,不可以被实例化,如果想在程序中获取一个Calendar实例,则需要调用Calendar类的静态方法getInstance()。通过调用getInstance()方法获取Calendar实例的具体示例如下。
Calendar calendar = Calendar.getInstance();
表中的大多数方法都用到了int类型的参数field, 该参数需要接收Calendar类中定义的常量值,这些常量值分别表示不同的字段, Calendar类常用的常量值如下所示。 Calendar.YEAR:用于获取当前年份。 Calendar.MONTH:用于获取当前月份,需要注意的是,在使Calendar.MONTH 字段时,月份的起始值是从0开始的,而不是从1开始,因此要获取当前的月 需要在Calendar.MONTH的基础上加1。 Calendar.DATE:用于获取当前日。 Calendar.HOUR:用于获取时。 Calendar.MINUTE:用于获取分。 Calendar.SECOND:用于获取秒。
import java.util.*; public class Example21 { public static void main(String[] args) { // 获取表示当前时间的Calendar对象 Calendar calendar = Calendar.getInstance(); int year = calendar.get(Calendar.YEAR); // 获取当前年份 int month = calendar.get(Calendar.MONTH) + 1; // 获取当前月份 int date = calendar.get(Calendar.DATE); // 获取当前日 int hour = calendar.get(Calendar.HOUR); // 获取时 int minute = calendar.get(Calendar.MINUTE); // 获取分 int second = calendar.get(Calendar.SECOND); // 获取秒 System.out.println("当前时间为:" + year + "年 " + month + "月 " + date + "日 "+ hour + "时 " + minute + "分 " + second + "秒"); } }
import java.util.*; public class Example22 { public static void main(String[] args) { Calendar calendar = Calendar.getInstance();// 获取表示当前时间的Calendar对象 calendar.set(2021, 1, 1);// 设置指定日期 calendar.add(Calendar.DATE, 100);// 为指定日期增加时间 int year = calendar.get(Calendar.YEAR);// 返回指定日期的年 int month = calendar.get(Calendar.MONTH) + 1;// 返回指定日期的月 int date = calendar.get(Calendar.DATE);// 返回指定日期的日 System.out.println("计划竣工日期为:" + year + "年" + month + "月" + date + "日"); } }
注意:Calendar.DATE表示的是天数,当天数累加到当月的最大值时,如果继续累加,Calendar.DATE的天数就会从1开始计数,同时月份值会自动加1,这和算术运算中的进位类似。
Instant类
Instant 类代表的是某个瞬间的时间。其内部由两个部分组成,第一部分保存的是标准Java计算时代(就是1970年1月1日开始)到现在的秒数,第二部分保存的是纳秒数。 Instant类提供了一系列用于操作时间常用的方法,Instant类常用的方法如下表所示。
import java.time.Instant; public class Example23 { public static void main(String[] args) { // Instant 类的时间戳类从1970-01-01 00:00:00 截止到当前时间的毫秒值 Instant now = Instant.now(); System.out.println("从系统获取的当前时刻为:"+now); Instant instant = Instant.ofEpochMilli(1000 * 60 * 60 * 24); System.out.println("计算机元年增加1000 * 60 * 60 * 24毫秒数后为:"+instant); Instant instant1 = Instant.ofEpochSecond(60 * 60 * 24); System.out.println("计算机元年增加60 * 60 * 24秒数后为:"+instant1); System.out.println("获取的秒值为:"+Instant.parse("2007-12- 03T10:15:30.44Z").getEpochSecond()); System.out.println("获取的纳秒值为:"+Instant.parse("2007-12-03T10:15:30.44Z").getNano()); System.out.println("从时间对象获取的Instant实例为:"+Instant.from(now)); } }
LocalDate类
LocalDate类表示不带时区的日期,它所表示的日期包括年份和月份两部分。LocalDate类不能代表时间线上的即时信息,只是描述日期。LocalDate类提供了两个获取日期对象的方法now()和of(int year, int month, int dayOfMonth),具体如下所示。
//按指定日期创建LocalDate对象 LocalDate date = LocalDate.of(2020, 12, 12); //从默认时区的系统时钟获取当前日期 LocalDate now1 = LocalDate.now();
public static void main(String[] args) { LocalDate now = LocalDate.now();//获取日期时分秒 LocalDate of = LocalDate.of(2015, 12, 12); System.out.println("1. LocalDate的获取及格式化的相关方法--------"); System.out.println("从LocalDate实例获取的年份为:"+now.getYear()); System.out.println("从LocalDate实例获取的月份为:"+now.getMonthValue()); System.out.println("从LocalDate实例获取当天为本月的第几天:"+now.getDayOfMonth()); System.out.println("将获取到的Loacaldate实例格式化为:"+ now.format(DateTimeFormatter.ofPattern("yyyy年MM月dd日"))); System.out.println("2. LocalDate判断的相关方法----------------"); System.out.println("判断日期of是否在now之前:"+of.isBefore(now)); System.out.println("判断日期of是否在now之后:"+of.isAfter(now)); System.out.println("判断日期of和now是否相等:"+now.equals(of)); System.out.println("判断日期of是否是闰年:"+ of.isLeapYear()); System.out.println("3. LocalDate解析以及加减操作的相关方法---------"); //给出一个符合默认格式要求的日期字符串 String dateStr="2020-02-01"; System.out.println("把日期字符串解析成日期对象后为"+ LocalDate.parse(dateStr)); System.out.println("将LocalDate实例年份加1为:"+now.plusYears(1)); System.out.println("将LocalDate实例天数减10为:"+now.minusDays(10)); System.out.println("将LocalDate实例指定年份为2014:"+now.withYear(2014)); }
LocalTime类
LocalTime类用来表示时间,通常表示的是小时分钟秒。与LocalDate类一样,LocalTime类不能代表时间线上的即时信息,只是时间的描述。LocalTime类中提供了获取时间对象的方法,与LocalDate类用法类似。 此外,LocalTime类也提供了时间格式化、增减时分秒等常用方法,这些方法与LocalDate类的方法用法相同,这里不再详细列举。
import java.time.LocalTime; import java.time.format.DateTimeFormatter; public class Example25 { public static void main(String[] args) { // 获取当前时间,包含毫秒数 LocalTime time = LocalTime.now(); LocalTime of = LocalTime.of(9,23,23); System.out.println("从LocalTime获取的小时为:"+time.getHour()); System.out.println("将获取到的LoacalTime实例格式化为:"+ time.format(DateTimeFormatter.ofPattern("HH:mm:ss"))); System.out.println("判断时间of是否在now之前:"+of.isBefore(time)); System.out.println("将时间字符串解析为时间对象后为:"+ LocalTime.parse("12:15:30")); System.out.println("从LocalTime获取当前时间,不包含毫秒数:"+time.withNano(0)); } }
注意:当我们调用parse()方法解析字符串的时候,该字符串要符合默认的时分秒格式。
LocalDateTime类
LocalDateTime类是LocalDate类与LocalTime类的综合,它既包含日期也包含时间,查看Java API可以知道,LocalDateTime类包含了LocalDate类与LocalTime类的所有方法。LocalDateTime类还额外提供了日期时间的转换方法。
import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; public class Example26 { public static void main(String[] args) { LocalDateTime now = LocalDateTime.now();//获取系统当前年月日,时分秒 System.out.println("获取的当前日期时间为:"+now); System.out.println("将目标LocalDateTime转换为相应的LocalDate实例:"+ now.toLocalDate()); System.out.println("将目标LocalDateTime转换为相应的LocalTime实例:"+ now.toLocalTime()); //指定格式 DateTimeFormatter ofPattern = DateTimeFormatter.ofPattern ("yyyy年MM月dd日 HH时mm分ss秒"); System.out.println("格式化后的日期时间为:"+now.format(ofPattern)); } }
Duration类
Duration类表示两个时间之间的间隔,间隔时间的单位可以是天、时、分、秒、毫秒和纳秒,例如今天的12:00:00与13:00:00之间,间隔1小时,或者60分钟,或者3600秒。Duration类的常用方法如下表所示。
import java.time.Duration; import java.time.LocalTime; public class Example27{ public static void main(String[] args) { LocalTime start = LocalTime.now(); LocalTime end = LocalTime.of(20,13,23); Duration duration = Duration.between(start, end); //间隔的时间 System.out.println("时间间隔为:"+duration.toNanos()+"纳秒"); System.out.println("时间间隔为:"+duration.toMillis()+"毫秒"); System.out.println("时间间隔为:"+duration.toHours()+"小时"); } }
Period类
Period类主要用于计算两个日期的间隔,与Duration类相同,Period类也是通过between()方法计算日期间隔,并提供了获取年月日的三个常用方法,分别是 getYears()、getMonths()和getDays()。
import java.time.LocalDate; import java.time.Period; public class Example28 { public static void main(String[] args) { LocalDate birthday = LocalDate.of(2018, 12, 12); LocalDate now = LocalDate.now(); //计算两个日期的间隔 Period between = Period.between(birthday, now); System.out.println("时间间隔为"+between.getYears()+"年"); System.out.println("时间间隔为"+between.getMonths()+"月"); System.out.println("时间间隔为"+between.getDays()+"天"); } }
日期与时间格式化类
DateFormat类
尽管使用java.util.Date类能够获取日期和时间,但是因为其显示格式与日常使用的日期格式不同,因此,Java提供了DateFormat类,DateFormat类可以将日期时间进行格式化,使日期时间的格式符合人们的阅读习惯。DateFormat是一个抽象类,不能被直接实例化,但它提供了一系列用于获取DateFormat类实例的静态方法,并能调用其他相应的方法进行操作。
FULL:用于表示完整格式的日期时间。 LONG:用于表示长格式的日期时间。 MEDIUM:用于表示普通格式的日期时间。 SHORT:用于表示短格式的日期时间。
public static void main(String[] args) { Date date = new Date();// 创建Date对象 // Full格式的日期格式器对象 DateFormat fullFormat = DateFormat.getDateInstance(DateFormat.FULL); // LONG格式的日期格式器对象 DateFormat longFormat = DateFormat.getDateInstance(DateFormat.LONG); // MEDIUM格式的日期/时间 格式器对象 DateFormat mediumFormat = DateFormat.getDateTimeInstance( DateFormat.MEDIUM, DateFormat.MEDIUM); // SHORT格式的日期/时间格式器对象 DateFormat shortFormat = DateFormat.getDateTimeInstance( DateFormat.SHORT, DateFormat.SHORT); System.out.println("当前日期的完整格式为:" + fullFormat.format(date)); System.out.println("当前日期的长格式为:" + longFormat.format(date)); System.out.println("当前日期的普通格式为:" + mediumFormat.format(date)); System.out.println("当前日期的短格式为:" + shortFormat.format(date)); }
import java.text.*; public class Example30 { public static void main(String[] args) throws ParseException { // 创建LONG格式的DateFormat对象 DateFormat dt = DateFormat.getDateInstance(DateFormat.LONG); // 定义日期格式的字符串 String str = "2021年05月20日"; // 输出对应格式的字符串解析成Date对象后的结果 System.out.println(dt.parse(str)); } }
SimpleDateFormat类
为了能够更好地格式化日期、解析字符串,Java提供了一个SimpleDateFormat类。 SimpleDateFormat类是DateFormat类的子类,它可以使用new关键字创建实例对象。在创建实例对象时,SimpleDateFormat类的构造方法需要接收一个表示日期格式模板的字符串参数,日期格式模板通过特定的日期标记可以将一个日期格式的日期数字提取出来。
import java.text.*; import java.util.*; public class Example31 { public static void main(String[] args) throws Exception { // 创建一个SimpleDateFormat对象 SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日"); // 按SimpleDateFormat对象的日期模板格式化Date对象 System.out.println(sdf.format(new Date())); } }
import java.text.*; import java.util.*; public class Example32 { public static void main(String[] args) throws Exception { String strDate = "2021-03-02 17:26:11.234"; //定义日期时间的字符串 String pat = "yyyy-MM-dd HH:mm:ss.SSS"; //定义日期时间的模板 // 创建一个SimpleDateFormat对象 SimpleDateFormat sdf = new SimpleDateFormat(pat); // 按SimpleDateFormat对象的日期模板将字符串格式化为Date对象 Date d = sdf.parse(strDate); System.out.println(d); } }
数字格式化类
Java提供了NumberFormat类,定义在java.text包中。NumberFormat类可以格式化和解析任何区域设置的数字,使数字的格式符合人们的阅读习惯。NumberFormat类是一个抽象类,不能被直接实例化,但是它提供了一系列用于获取NumberFormat类实例的静态方法,并能调用其他相应的方法进行操作。
包装类
Java程序设计提倡一种思想,即万物皆对象。这样就出现一个矛盾,因为Java中的数据类型分为基本数据类型和引用数据类型,很多类的方法都需要接收引用类型的对象,此时就无法将一个基本数据类型的值传入。为了解决这样的问题,就需要将基本数据类型值进行包装,即将基本数据类型值包装为引用数据类型的对象。能够将基本数据类型值包装为引用数据类型对象的类,称为包装类。JDK提供了一系列包装类,通过这些包装类可以将基本数据类型的值包装为引用数据类型的对象。
除了Character和Boolean是Object的直接子类外,Integer、Byte、Float、Double、Short、Long都属于Number类的子类。Number类是一个抽象类,其本身提供了一系列的返回以上6种基本数据类型的方法,Number类的方法主要是将数字包装类中的内容变为基本数据类型。
将一个基本数据类型转变为包装类的过程,称为装箱操作,反之,将一个包装类转变为基本数据类型的过程称为拆箱操作。
public class Example33 { public static void main(String args[]) { int a = 20; //声明一个基本数据类型 Integer in = new Integer(a); //装箱:将基本数据类型变为包装类 System.out.println(in); int temp = in.intValue(); //拆箱:将一个包装类变为基本数据类型 System.out.println(temp); } }
Integer类特有的方法
其中的intValue()方法可以将Integer类型的值转为int类型,这个方法可以用来进行手动拆箱操作。parseInt(String s)方法可以将一个字符串形式的数值转成int类型,valueOf(int i)可以返回指定的int值为Integer实例。
public class Example34 { public static void main(String args[]) { Integer num = new Integer(20); //手动装箱 int sum = num.intValue() + 10; //手动拆箱 System.out.println("将Integer类值转化为int类型后与10求和为:"+ sum); System.out.println("返回表示10的Integer实例为:" + Integer.valueOf(10)); int w = Integer.parseInt("20")+32; System.out.println("将字符串转化为整数位:" + w); } }
(1)包装类都重写了Object类中的toString()方法, 以字符串的形式返回被包装的基本数据类型的值。 (2)除了Character外,包装类都有valueOf(String s)方法, 可以根据String类型的参数创建包装类对象,但参数字符串s不能为null, 而且字符串必须是可以解析为相应基本类型的数据,否则虽然编译通过,但运行时会报错。 具体示例如下。 Integer i = Integer.valueOf("123"); // 合法 Integer i = Integer.valueOf("12a"); // 不合法,12a不能被正确解析为基本类型数据 (3)除了Character外,包装类都有parseXxx(String s)的静态方法, 该方法的作用是将字符串转换为对应的基本类型的数据。 参数s不能为null,而且同样字符串必须可以解析为相应基本类型的数据, 否则虽然编译通过,但运行时会报错。具体示例如下。 int i = Integer.parseInt("123"); // 合法 Integer in = Integer.parseInt("itcast"); // 不合法
正则表达式
在实际开发中,经常需要对用户输入的信息进行格式校验。例如,判断输入的字符串是否符合Email格式。若手工编写代码实现校验逻辑,不仅耗时,而且健壮性也往往得不到保证。为此,Java提供了正则表达式,通过正则表达式可以快速校验信息格式。本节将针对正则表达式进行详细地讲解。
正则表达式语法
正则表达式是由普通字符(如字符a~z)和特殊字符(元字符)组成的文本模式,例如,正则表达式“[a-z]*”描述了所有仅包含小写字母的字符串,其中a、z为普通字符,短横线、左右中括号及星号则为元字符。
1. 点号
点号可以匹配除“\n”之外的任何单个字符。 例如,正则表达式“t.n”可匹配“tan”“ten”“tcn”“t=n”“t n”(t和n之间有一个空格)等。
2. 中括号
中括号可以匹配中括号内所有字符中的任意一个。 可以在中括号内指定需要匹配的若干字符,表示仅使用这些字符参与匹配。 例如,正则表达式“t[abcd]n”只匹配“tan”“tbn”“tcn”“tdn”。 中括号还有一些特殊写法,用于匹配某一范围内的字符,例如“[a-z]”匹配一个小写字母, “[a-zA-Z]”匹配一个字母、“[0-9]”匹配一个数字字符、 “[a-z0-9]”匹配一个小写字母或一个数字字符等。
3.“|”符号
“|”符号可以匹配其左侧或右侧的符号。 例如,正则表达式“t(a|e|i|io)n”,除了“tan”“ten”和“tin”外,还可以匹配“tion”。 使用“|”符号时,必须使用圆括号将可以匹配的字符括起来, 圆括号用来标记正则表达式中的组(Group)。
4.“^”符号
“^”符号可以匹配一行的开始。 例如,正则表达式“^Spring.*”匹配“Spring MVC”,而不匹配“a Spring MVC”。 若“^”符号在中括号内,则表示不需要参与匹配的字符。 例如,正则表达式“[a-z&&[^bc]]”表示,可以匹配除b和c之外的小写字母,等价于“[ad-z]”, 正则表达式“[a-z&&[^h-n]]”表示除h到n之外的小写字母,等价于“[a-go-z]”, 正则表达式“[^b][a-z]+”表示首个字符不能是b且后跟至少一个小写字母。
5.“%”符号
“$”符号可以匹配一行的结束。 例如,正则表达式“.*App$”中的“$”符号表示匹配以App结尾的字符串, 可以匹配“Andriod App”,而不匹配“iOS Apps”和“App.”。
6.“\”符号
“\”符号表示其后的字符是普通字符而非元字符。 例如,正则表达式“\$”用来匹配“$”字符而非结束,“\.”用来匹配“.”字符而非任一字符。
Pattern类
Pattern类用于创建一个正则表达式,也可以说创建一个匹配模式。Pattern类的构造方法是私有的,不可以直接创建正则表达式,为此,Pattern类提供了一个静态的complie()方法,通过调用complie()方法可以创建一个正则表达式,具体代码如下所示。
Pattern p = Pattern.compile("\\w+");
import java.util.regex.Matcher; import java.util.regex.Pattern; public class Example35 { public static void main(String[] args) { Pattern p1 = Pattern.compile("a*b"); //根据参数指定的正则表达式创建模式 Matcher m1 = p1.matcher("aaaaab"); //获取目标字符串的匹配器 Matcher m2 = p1.matcher("aaabbb"); //获取目标字符串的匹配器 System.out.println(m1.matches()); //执行匹配器 System.out.println(m2.matches()); //执行匹配器 Pattern p2 = Pattern.compile("[/]+"); String[] str = p2.split("张三//李四/王五//赵六/钱七"); //按模式分割字符串 for(String s : str){ System.out.print(s+"\t"); } } }
Matcher类
Matcher类用于验证Pattern定义的模式与字符串是否匹配,因此Matcher实例也称为匹配器。Matcher类的构造方法也是私有的,不能直接创建Macher实例,只能通过Pattern.matcher()方法获取该类的实例,多个Matcher对象可以使用同一Pattern对象。
public static void main(String[] args) { Pattern p=Pattern.compile("\\d+"); Matcher m=p.matcher("22bb23"); System.out.println("字符串是否匹配:"+ m.matches()); Matcher m2=p.matcher("2223"); System.out.println("字符串2223与模式p是否匹配:"+ m2.matches()); System.out.println("字符串22bb23与模式p的匹配结果:"+ m.lookingAt()); Matcher m3=p.matcher("aa2223"); System.out.println("字符串22bb23与模式p的匹配结果:"+m3.lookingAt()); System.out.println("字符串22bb23与模式p是否存在下一个匹配结果:m.find()); m3.find();//返回true System.out.println("字符串aa2223与模式p是否存在在下一个匹配结果"+m3.find()); Matcher m4=p.matcher("aabb"); System.out.println("字符串aabb与模式p是否存在下一个匹配结果:"+ m4.find()); Matcher m1=p.matcher("aaa2223bb"); m1.find();//匹配2223 System.out.println("模式p与字符串aaa2223bb的匹配的起始索引:"+ m1.start()); System.out.println("模式p与字符串aaa2223bb的最后一个字符匹配后的偏移量"+ m1.end()); System.out.println("模式p与字符串aaa2223bb的匹配到的子字符串:"+ m1.group()); Pattern p2 = Pattern.compile("[/]+"); Matcher m5 = p2.matcher("张三/李四//王五///赵六"); System.out.println("将字符串张三/李四//王五///赵六中的/全部替换为|:"+ m5.replaceAll("|")); System.out.println("将字符串张三/李四//王五///赵六中的首个/替换为|:"+ m5.replaceFirst("|")); }
String类对正则表达式的支持
public class Example37{ public static void main(String[] args) { String str = "A1B22DDS34DSJ9D".replaceAll("\\d+","_"); System.out.println("字符串替换后为:"+str); boolean te = "321123as1".matches("\\d+"); System.out.println("字符串是否匹配:"+te); String[] s="SDS45d4DD4dDS88D".split("\\d+"); System.out.print("字符串拆分后为:"); for(int i=0;i<s.length;i++){ System.out.print(s[i]+" "); } } }
注意:String类matches(String regex)方法的调用同Pattern类和Matcher类中该方法调用一样,必须匹配所有的字符串才返回true,否则返回false。
集合
为了存储不同类型的多个对象,Java提供了一系列特殊的类,这些类可以存储任意类型的对象,并且存储的长度可变,被统称为集合。集合可以简单理解为一个长度可变,可以存储不同数据类型的动态数组。集合都位于java.util包中,使用集合时必须导入java.util包。
虚线框里都是接口类型,实线框里是具体的实现类。
Collection接口
Collection接口是Java单列集合中的根接口,它定义了各种具体单列集合的共性,其他 单列集合大多直接或间接继承该接口
public interface Collection<E> extends Iterable<E>{ //Query Operations }
由上述Collection接口的定义可以看到,Collection是Iterable的子接口,Collection和 Iterable后面的<E>表示它们都使用了泛型。
Arrays.asList(数组)返回一个集合
List接口
List接口继承自Collection接口,List接口实例中允许存储重复的元素,所有的元素以线性方式进行存储。在程序中可以通过索引访问List接口实例中存储的元素。另外,List接口实例中存储的元素是有序的,即元素的存入顺序和取出顺序一致。
List作为Collection集合的子接口,不但继承了Collection接口中的全部方法,而且还增加了一些根据元素索引操作集合的特有方法。
ArrayList类
ArrayList是List接口的一个实现类,它是程序中最常见的一种集合。ArrayList集合内部封装了一个长度可变的数组对象,当存入的元素超过数组长度时,ArrayList会在内存中分配一个更大的数组来存储这些元素,因此可以将ArrayList集合看作一个长度可变的数组。
泛型支持:
E
表示列表的元素类型public class Example01 { public static void main(String[] args) { ArrayList list = new ArrayList(); // 创建ArrayList集合 list.add("张三"); // 向集合中添加元素 list.add("李四"); list.add("王五"); list.add("赵六"); System.out.println("集合的长度:" + list.size()); //获取集合中元素的个数 System.out.println("第2个元素是:" + list.get(1)); //取出并打印指定位置的元素 list.remove(3); //删除索引为3的元素 System.out.println("删除索引为3的元素:"+list); list.set(1,"李四2"); //替换索引为1的元素为李四2 System.out.println("替换索引为1的元素为李四2:"+list); } }
// 1. 默认构造方法(初始容量10) ArrayList<String> list1 = new ArrayList<>(); // 2. 指定初始容量 ArrayList<Integer> list2 = new ArrayList<>(20); // 3. 通过集合初始化 List<String> tempList = Arrays.asList("A", "B", "C"); ArrayList<String> list3 = new ArrayList<>(tempList); // 包含 ["A", "B", "C"]
ArrayList<String> list = new ArrayList<>(); // 添加到末尾 list.add("Apple"); // ["Apple"] list.add("Banana"); // ["Apple", "Banana"] // 在指定索引插入 list.add(1, "Orange"); // ["Apple", "Orange", "Banana"] // 添加整个集合 List<String> fruits = Arrays.asList("Mango", "Grape"); list.addAll(fruits); // ["Apple", "Orange", "Banana", "Mango", "Grape"]
ArrayList<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "D")); // 按索引删除 list.remove(1); // 移除 "B" → ["A", "C", "D"] // 按元素值删除 list.remove("C"); // 移除 "C" → ["A", "D"] // 清空列表 list.clear(); // []
ArrayList<Integer> nums = new ArrayList<>(Arrays.asList(10, 20, 30, 20)); // 获取元素 int num = nums.get(0); // 10 // 判断包含 boolean has20 = nums.contains(20); // true // 查找索引 int firstIndex = nums.indexOf(20); // 1 int lastIndex = nums.lastIndexOf(20); // 3 // 检查空列表 boolean isEmpty = nums.isEmpty(); // false int size = nums.size(); // 4
ArrayList<String> list = new ArrayList<>(Arrays.asList("X", "Y", "Z")); // 替换元素 String oldVal = list.set(1, "W"); // "Y" → ["X", "W", "Z"] // 排序 list.sort(Comparator.reverseOrder()); // ["Z", "W", "X"] // 批量替换(所有元素转大写) list.replaceAll(s -> s.toUpperCase()); // ["Z", "W", "X"]
ArrayList<String> list = new ArrayList<>(Arrays.asList("A", "B", "C")); // 转为数组 String[] array1 = list.toArray(new String[0]); // ["A", "B", "C"] // 遍历:for循环 for (String s : list) { System.out.println(s); } // 遍历:forEach + Lambda list.forEach(s -> System.out.println(s)); // 使用迭代器 Iterator<String> it = list.iterator(); while (it.hasNext()) { String s = it.next(); if (s.equals("B")) { it.remove(); // 安全删除当前元素 } }
ArrayList<Integer> nums = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5)); // 获取子列表(左闭右开) List<Integer> subList = nums.subList(1, 4); // [2, 3, 4] // 保留交集 nums.retainAll(Arrays.asList(2, 4, 6)); // nums → [2, 4] // 删除交集 nums.removeAll(Arrays.asList(4)); // nums → [2]
// 确保容量(避免频繁扩容) ArrayList<Integer> list = new ArrayList<>(); list.ensureCapacity(100); // 一次性分配100的容量 // 缩容到实际大小 list.trimToSize();
由于ArrayList集合的底层是使用一个数组来存储元素,在增加或删除指定位置的元素时,会创建新的数组,效率比较低,因此Arraylist集合不适合做大量的增删操作,而适合元素的查找。
LinkedList类
为了克服ArrayList集合在查询元素时速度很快,但在增删元素时效率较低的局限性,可以使用List接口的另一个实现类LinkedList。LinkedList集合内部维护了一个双向循环链表,链表中的每一个元素都使用引用的方式记录它的前一个元素和后一个元素,从而可以将所有的元素彼此连接起来。当插入一个新元素时,只需要修改元素之间的引用关系即可,删除一个节点也是如此。正因为这样的存储结构,所以LinkedList集合增删效率非常高。
public static void main(String[] args) { LinkedList link = new LinkedList(); // 创建LinkedList集合 link.add("张三"); link.add("李四"); link.add("王五"); link.add("赵六"); System.out.println(link.toString()); // 获取并打印该集合中的元素 link.add(3, "Student"); // 向link集合中索引3处插入元素Student link.addFirst("First"); // 向link集合第一个位置插入元素First System.out.println(link); System.out.println(link.getFirst()); // 取出link集合中第一个元素 link.remove(3); // 移除link集合中指定索引位置为3的元素 link.removeFirst(); // 移除link集合中第一个元素 System.out.println(link); }
集合遍历
Iterator接口
Iterator接口是Java集合框架中的一员,但它与Collection、Map接口有所不同,Collection接口与Map接口主要用于存储元素,而Iterator主要用于迭代访问(遍历)Collection中的元素,通常情况下Iterator对象也被称为迭代器。
public class Example03 { public static void main(String[] args) { ArrayList list = new ArrayList(); // 创建ArrayList集合 list.add("张三"); // 向该集合中添加字符串 list.add("李四"); list.add("王五"); list.add("赵六"); Iterator it = list.iterator(); // 获取Iterator对象 while (it.hasNext()) { // 判断ArrayList集合中是否存在下一个元素 Object obj = it.next(); // 取出ArrayList集合中的元素 System.out.println(obj); } } }
注意:通过迭代器获取ArrayList集合中的元素时,这些元素的类型都是Object类型,如果 想获取到特定类型的元素,则需要进行对数据类型强制转换。
在使用Iterator迭代器对集合中的元素进行迭代时,如果调用了集合对象的remove()方法删除元素,之后继续使用迭代器遍历元素,会出现异常。
ArrayList list = new ArrayList(); //创建ArrayList集合 list.add("张三"); list.add("李四"); list.add("王五"); Iterator it = list.iterator(); // 获得Iterator对象 while (it.hasNext()) { // 判断该集合是否有下一个元素 Object obj = it.next(); // 获取该集合中的元素 if ("张三".equals(obj)) { // 判断该集合中的元素是否为张三 list.remove(obj); // 删除该集合中的元素 } } System.out.println(list);
在运行时抛出了并发修改异常ConcurrentModificationException。这个异常是迭代器对象抛出的,出现异常的原因是集合在迭代器运行期间删除了元素,会导致迭代器预期的迭代次数发生改变,迭代器的迭代结果不准确。要解决上述问题,有两种方式可以采用。
并发修改异常解决方式一
从业务逻辑上讲只想将姓名为张三的学生删除,因此只需找到该学生后跳出循环不再迭代即可,也就是在案例代码“list.remove(obj);”下面增加一个break语句,代码如下。
if ("张三".equals(obj)) { list.remove(obj); break; }
在使用break语句跳出循环以后,由于没有继续使用迭代器对集合中的元素进行迭代,所以集合中删除元素对程序没有任何影响,就不会再出现异常。
并发修改异常解决方式二
如果需要在集合的迭代期间对集合中的元素进行删除,可以使用迭代器本身的删除方法,将案例代码“list.remove(obj);”替换成it.remove()即可解决这个问题,代码如下。
if ("张三".equals(obj)) { it.remove(); }
foreach循环
foreach循环是一种更加简洁的for循环。foreach循环用于遍历数组或集合中的元素, 语法格式如下所示:
for(容器中元素类型 临时变量:容器变量) { 执行语句 }
import java.util.*; public class Example05 { public static void main(String[] args) { ArrayList list = new ArrayList(); // 创建ArrayList集合 list.add("张三"); // 向ArrayList集合中添加字符串元素 list.add("李四"); list.add("王五"); for (Object obj : list) { // 使用foreach循环遍历ArrayList对象 System.out.println(obj);// 取出并打印ArrayList集合中的元素 } } }
foreach循环虽然书写起来很简洁,但在使用时也存在一定的局限性。当使用foreach循环遍历集合和数组时,只能访问集合中的元素,不能对其中的元素进行修改。
static String[] strs = { "aaa", "bbb", "ccc" }; public static void main(String[] args) { // foreach循环遍历数组 for (String str : strs) { str = "ddd"; } System.out.println("foreach循环修改后的数组:" + strs[0] + "," + strs[1] + ","+ strs[2]); // for循环遍历数组 for (int i = 0; i < strs.length; i++) { strs[i] = "ddd"; } System.out.println("普通for循环修改后的数组:" + strs[0] + "," + strs[1] + ","+ strs[2]); }
从上述图中的运行结果可以看出foreach循环并不能修改数组中元素的值。原因是第6行代码中的str = "ddd"只是将临时变量str赋值为了一个新的字符串,这和数组中的元素没有一点关系。而在普通for循环中,可以通过索引的方式引用数组中的元素并修改其值。
Set接口
Set接口也继承自Collection接口,它与Collection接口中的方法基本一致,并没有对Collection接口进行功能上的扩充。与List接口不同的是,Set接口中元素是无序的,并且都会以某种规则保证存入的元素不出现重复。 Set接口常见的实现类有3个,分别是HashSet、LinkedHashSet、TreeSet。其中,HashSet根据对象的哈希值来确定元素在集合中的存储位置,具有良好的存取和查找性能;LinkedHashSet是链表和哈希表组合的一个数据存储结构;TreeSet则是以二叉树的方式存储元素,它可以对集合中的元素进行排序。
HashSet
HashSet是Set接口的一个实现类,它所存储的元素是不可重复的。当向HashSet集合中添加一个元素时,首先会调用该元素的hashCode()方法来确定元素的存储位置,然后再调用元素对象的equals()方法来确保该位置没有重复元素。Set集合与List集合存取元素的方式都一样,但是Set集合中的元素是无序的。
(1) 不允许重复元素
如果尝试添加重复元素,add()会false
(2) 无序存储
(3) 允许
null
值(4) 高性能查找(
O(1)
时间复杂度)
class Student { ......//省略声明变量id和name,省略有参构造方法 public String toString() {// 重写toString()方法 return id+":"+name; } } public class Example08 { public static void main(String[] args) { HashSet hs = new HashSet(); // 创建HashSet集合 Student stu1 = new Student("1", "张三"); // 创建Student对象 Student stu2 = new Student("2", "李四"); Student stu3 = new Student("2", "李四"); hs.add(stu1); hs.add(stu2); hs.add(stu3); System.out.println(hs); } }
由上图可知,运行结果中出现了两个相同的学生信息“2:李四”,这样的学生信息应该被视为重复元素,不允许同时出现在HashSet集合中。上面文件之所以没有去掉这样的重复元素,是因为在定义Student类时没有重写hashCode()和equals()方法。
// 重写hashCode方法 public int hashCode() { return id.hashCode(); // 返回id属性的哈希值 } // 重写equals方法 public boolean equals(Object obj) { if (this == obj) { // 判断是否是同一个对象 return true; // 如果是,直接返回true } if (!(obj instanceof Student)) { // 判断对象是为Student类型 return false; } Student stu = (Student) obj; // 将对象强转为Student类型 boolean b = this.id.equals(stu.id); // 判断id值是否相同 return b; // 返回判断结果 }
LinkedHashSet
HashSet集合存储的元素是无序的,如果想让元素的存取顺序一致,可以使用Java提供的LinkedHashSet类,LinkedHashSet类是HashSet的子类,与LinkedList一样,它也使用双向链表来维护内部元素的关系。
LinkedHashSet 是 Java 集合框架中的一个类,它继承自 HashSet,并实现了 Set 接口。与 HashSet 不同,LinkedHashSet 维护了元素的插入顺序,同时仍然保持 O(1) 时间复杂度的查找性能。
// 1. 默认构造方法(初始容量16,负载因子0.75) LinkedHashSet<String> set1 = new LinkedHashSet<>(); // 2. 指定初始容量 LinkedHashSet<Integer> set2 = new LinkedHashSet<>(20); // 3. 指定初始容量和负载因子 LinkedHashSet<Double> set3 = new LinkedHashSet<>(30, 0.8f); // 4. 通过已有集合初始化 List<String> list = Arrays.asList("A", "B", "C"); LinkedHashSet<String> set4 = new LinkedHashSet<>(list); // 保持插入顺序
TreeSet
TreeSet是Set接口的另一个实现类,它内部采用红黑树来存储元素,这样的结构可以保证TreeSet集合中没有重复的元素,并且可以对元素进行排序。
构造方法
默认构造方法:创建一个空的TreeSet,按照元素的自然顺序进行排序
TreeSet<Integer> treeSet = new TreeSet<>();
元素必须实现Comparable接口
带Comparator参数的构造方法:创建一个空的TreeSet,根据指定的比较器进行排序
TreeSet<String> treeSet = new TreeSet<>(new Comparator<String>() { @Override public int compare(String o1, String o2) { return o1.compareToIgnoreCase(o2); } });
带Collection参数的构造方法:创建一个包含指定集合元素的TreeSet,按照自然顺序排序
List<Integer> list = Arrays.asList(1, 2, 6, 7, 2, 8); TreeSet<Integer> treeSet = new TreeSet<>(list);
带SortedSet参数的构造方法
SortedSet<Integer> sortedSet = new TreeSet<>(Arrays.asList(1, 3, 5)); TreeSet<Integer> treeSet = new TreeSet<>(sortedSet);
TreeSet的排序规则
自然排序
自然排序要求向TreeSet集合中存储的元素所在类必须实现Comparable接口,并重写compareTo()方法,然后TreeSet集合就会对该类型元素使用compareTo()方法进行比较。compareTo()方法将当前对象与指定的对象进行顺序比较,返回值为一个整数,其中返回负整数、零或正整数分别表示当前对象小于、等于或大于指定对象,默认根据比较结果顺序排列。
import java.util.TreeSet; class Student implements Comparable{ ......//省略声明属性name和age,重写toString()方法 //重写Comparable接口的compareTo()方法 public int compareTo(Object obj) { Student stu = (Student)obj; //定义比较方式,先比较age,再比较name if(this.age - stu.age > 0){ return 1; } if(this.age - stu.age == 0){ return this.name.compareTo(stu.name); } return -1; } }
自定义排序
如果不想实现Comparable接口或者不想按照实现了Comparable接口的类中compareTo()方法的规则进行排序,可以通过自定义比较器的方式对TreeSet集合中的元素自定义排序规则。实现Comparator接口的类都是一个自定义比较器,可以在自定义比较器中的compare()方法中自定义排序规则。
// 按字符串长度排序 class LengthComparator implements Comparator<String> { @Override public int compare(String s1, String s2) { int result = s1.length() - s2.length(); return result == 0 ? s1.compareTo(s2) : result; } } // 使用 TreeSet<String> set = new TreeSet<>(new LengthComparator()); set.add("apple"); set.add("banana"); set.add("pear"); // 也可以使用匿名内部类或Lambda表达式 TreeSet<Student> set2 = new TreeSet<>((o1, o2) -> { return o1.getAge() - o2.getAge(); });
Map接口
Map接口是一种双列集合,它的每个元素都包含一个键对象Key和值对象Value,键和值对象之间存在一种对应关系,称为映射。Map中键对象Key不允许重复,访问Map集合中的元素时,只要指定了Key,就能找到对应的Value。
HashMap类
HashMap集合是Map接口的一个实现类,HashMap集合中的大部分方法都是Map接口方法的实现。在开发中,通常把HashMap集合对象的引用赋值给Map接口变量,那么接口变量就可以调用类实现的接口方法。HashMap集合用于存储键值映射关系,但HashMap集合没有重复的键并且键值无序。
Map中的键必须是唯一的,不能重复,如果存储了相同的键,后存储的值则会覆盖原有的值,简而言之就是,键相同,值覆盖。
跟c++不同,这个可以存NULL值
public static void main(String[] args) { Map map = new HashMap(); // 创建HashMap集合 map.put("1", "张三"); // 存储键和值 map.put("3", "李四"); map.put("2", "王五"); map.put("4", "赵六"); System.out.println("集合大小为:"+map.size()); System.out.println("判断是否包含传入的键(2):"+map.containsKey("2")); System.out.println("判断是否包含传入的值(王五):"+map.containsValue("王五")); System.out.println("移除键为1的值是:"+map.remove("1")); Collection values = map.values(); Iterator it = values.iterator(); while (it.hasNext()) { Object value = it.next(); System.out.println(value); } }
HashMap<String, Integer> map = new HashMap<>(); map.put("Alice", 25); // 添加键值对 map.put("Bob", 30); int age = map.get("Alice"); // 获取值(25) map.remove("Bob"); // 删除键值对 map.containsKey("Alice"); // true map.forEach((k, v) -> System.out.println(k + ": " + v)); // 遍历
第一种取出Map中所有的键和值的实现方式
先遍历Map集合中所有的键,再根据键获取相应的值。
public class Example15 { public static void main(String[] args) { Map map = new HashMap(); // 创建HashMap集合 map.put("1", "张三"); // 存储键和值 map.put("2", "李四"); map.put("3", "王五"); Set keySet = map.keySet(); // 获取键的集合 Iterator it = keySet.iterator(); // 获取Iterator对象 while (it.hasNext()) { Object key = it.next(); Object value = map.get(key); // 获取每个键所对应的值 System.out.println(key + ":" + value); } } }
第二种取出Map中所有的键和值的实现方式
先获取集合中所有的映射关系,然后从映射关系中取出键和值。
public static void main(String[] args) { Map map = new HashMap(); // 创建HashMap集合 map.put("1", "张三"); // 存储键和值 map.put("2", "李四"); map.put("3", "王五"); Set entrySet = map.entrySet(); Iterator it = entrySet.iterator(); // 获取Iterator对象 while (it.hasNext()) { // 获取集合中键值对映射关系 Map.Entry entry = (Map.Entry) (it.next()); Object key = entry.getKey(); // 获取Entry中的键 Object value = entry.getValue();// 获取Entry中的值 System.out.println(key + ":" + value); } }
LinkedHashMap
HashMap集合迭代出来元素的顺序和存入的顺序是不一致的。如果想让这Map集合中的元素迭代顺序与存入顺序一致,可以使用LinkedHashMap集合,LinkedHashMap是HashMap的子类,与LinkedList一样,LinkedHashMap集合也使用双向链表维护内部元素的关系,使Map集合元素迭代顺序与存入顺序一致。
LinkedHashMap<String, Integer> map = new LinkedHashMap<>(); map.put("Alice", 25); map.put("Bob", 30); map.put("Charlie", 35); // 遍历顺序:Alice → Bob → Charlie for (Map.Entry<String, Integer> entry : map.entrySet()) { System.out.println(entry.getKey() + ": " + entry.getValue()); }
LinkedHashMap<String, Integer> lruCache = new LinkedHashMap<>(16, 0.75f, true); lruCache.put("A", 1); lruCache.put("B", 2); lruCache.put("C", 3); lruCache.get("A"); // 访问 A,使其移动到末尾 lruCache.put("D", 4); // 插入 D // 遍历顺序:B → C → A → D(最旧的 B 在最前) for (Map.Entry<String, Integer> entry : lruCache.entrySet()) { System.out.println(entry.getKey() + ": " + entry.getValue()); }
final int MAX_SIZE = 3; LinkedHashMap<String, Integer> lruCache = new LinkedHashMap<>(16, 0.75f, true) { @Override protected boolean removeEldestEntry(Map.Entry<String, Integer> eldest) { return size() > MAX_SIZE; // 当大小超过 MAX_SIZE 时,删除最旧的元素 } }; lruCache.put("A", 1); lruCache.put("B", 2); lruCache.put("C", 3); lruCache.put("D", 4); // 插入 D 后,A 被移除 System.out.println(lruCache); // 输出:{B=2, C=3, D=4}
TreeMap
HashMap集合存储的元素的键值是无序的和不可重复的,为了对集合中的元素的键值进行排序,Map接口还有了另一个可以对集合中元素键和值进行排序的实现类TreeMap。
TreeMap中的键必须是唯一的,不能重复并且有序,如果存储了相同的键,后存储的值则会覆盖原有的值。
TreeMap<String, Integer> treeMap = new TreeMap<>(); treeMap.put("Alice", 25); treeMap.put("Bob", 30); treeMap.put("Charlie", 35); // 遍历顺序:Alice → Bob → Charlie(字典序) for (Map.Entry<String, Integer> entry : treeMap.entrySet()) { System.out.println(entry.getKey() + ": " + entry.getValue()); }
Comparator<String> lengthComparator = (s1, s2) -> s1.length() - s2.length(); TreeMap<String, Integer> customTreeMap = new TreeMap<>(lengthComparator); customTreeMap.put("Alice", 25); customTreeMap.put("Bob", 30); customTreeMap.put("Charlie", 35); // 遍历顺序:Bob → Alice → Charlie(按字符串长度) for (Map.Entry<String, Integer> entry : customTreeMap.entrySet()) { System.out.println(entry.getKey() + ": " + entry.getValue()); }
TreeMap<String, Integer> reverseTreeMap = new TreeMap<>(Comparator.reverseOrder()); reverseTreeMap.put("Alice", 25); reverseTreeMap.put("Bob", 30); reverseTreeMap.put("Charlie", 35); // 遍历顺序:Charlie → Bob → Alice(逆字典序) for (Map.Entry<String, Integer> entry : reverseTreeMap.entrySet()) { System.out.println(entry.getKey() + ": " + entry.getValue()); }
TreeMap<Integer, String> scoreMap = new TreeMap<>(); scoreMap.put(90, "Alice"); scoreMap.put(80, "Bob"); scoreMap.put(70, "Charlie"); scoreMap.put(60, "David"); // 获取 [70, 90) 的子映射 SortedMap<Integer, String> subMap = scoreMap.subMap(70, 90); System.out.println(subMap); // 输出:{70=Charlie, 80=Bob}
class Student { private String name; private int age; //省略getter/setter方法 public Student(String name, int age) { super(); this.name = name; this.age = age; } public String toString() { return "Student [name=" + name + ", age=" + age + "]"; } } public static void main(String[] args) { TreeMap tm = new TreeMap(new Comparator<Student>() { public int compare(Student s1, Student s2) { int num = s1.getName().compareTo(s2.getName());//按照姓名比较 return num == 0 ? num:s1.getAge() - s2.getAge(); } }); tm.put(new Student("张三", 23), "北京"); tm.put(new Student("李四", 13), "上海"); tm.put(new Student("赵六", 43), "深圳"); tm.put(new Student("王五", 33), "广州"); Set keySet = tm.keySet(); Iterator it = keySet.iterator(); while (it.hasNext()) { Object key = it.next(); Object value = tm.get(key); // 获取每个键所对应的值 System.out.println(key+":"+value); } }
Properties类
Map接口还有一个实现类Hashtable,它和HashMap十分相似,区别在于Hashtable是线程安全的。Hashtable存取元素时速度很慢,目前基本上被HashMap类所取代。但Hashtable类有一个很重要的子类Properties,应用非常广泛。Properties主要用于存储字符串类型的键和值,在实际开发中,经常使用Properties集合存储应用的配置项。
----------------------------------------------------------
假设有一个文本编辑工具,要求默认背景色是红色,字体大小为14px,语言为中文,其配置项如下面的代码。 要求使用Properties集合对这些配置项进行存储。
Backgroup-color = red Font-size = 14px Language = chinese
import java.util.*; public class Example21 { public static void main(String[] args) { Properties p=new Properties(); // 创建Properties对象 p.setProperty("Backgroup-color", "red"); p.setProperty("Font-size", "14px"); p.setProperty("Language", "chinese"); Enumeration names = p.propertyNames();//获取Enumeration对象所有键枚举 while(names.hasMoreElements()){ //循环遍历所有的键 String key=(String) names.nextElement(); String value=p.getProperty(key); // 获取对应键的值 System.out.println(key+" = "+value); } } }
常用工具类
Collections工具类
Collections类提供了一系列方法用于对List集合进行添加和排序操作,常用的方法如下表所示。
import java.util.ArrayList; import java.util.Collections; public class Example22 { public static void main(String[] args) { ArrayList<String> list = new ArrayList<>(); Collections.addAll(list, "C","Z","B","K"); // 添加元素 System.out.println("排序前: " + list); Collections.reverse(list); // 反转集合 System.out.println("反转后: " + list); Collections.sort(list); // 按自然顺序排列 System.out.println("按自然顺序排序后: " + list); Collections.shuffle(list); // 随机打乱集合元素 System.out.println("按随机顺序排序后: " + list); Collections.swap(list, 0, list.size()-1); // 将集合首尾元素交换 System.out.println("集合首尾元素交换后: " + list); } }
Collections类还提供了一些常用方法用于操作Set集合、 List集合和Map集合等,常用的方法如下表所示。
import java.util.ArrayList; import java.util.Collections; public class Example23 { public static void main(String[] args) { ArrayList<Integer> list = new ArrayList<>(); Collections.addAll(list, -3,2,9,5,8);// 向集合中添加所有指定元素 System.out.println("集合中的元素: " + list); System.out.println("集合中的最大元素: " + Collections.max(list)); System.out.println("集合中的最小元素: " + Collections.min(list)); Collections.replaceAll(list, 8, 0); // 将集合中的8用0替换掉 System.out.println("替换后的集合: " + list); Collections.sort(list); //使用二分查找前,必须保证元素有序 System.out.println("集合排序后为: "+list); int index = Collections.binarySearch(list, 9); System.out.println("集合通过二分查找方法查找元素9所在索引为:"+index); } }
Arrays工具类
Arrays工具类中的静态方法sort()来实现数组排序。
public static void printArray(int[] arr) { System.out.print("["); for (int x = 0; x < arr.length; x++) { if (x != arr.length - 1) { System.out.print(arr[x] + ", "); } else { System.out.println(arr[x] + "]"); } } }
import java.util.Arrays; public class Example24 { public static void main(String[] args) { int[] arr = { 9, 8, 3, 5, 2 }; // 初始化一个数组 System.out.print("排序前:"); printArray(arr); // 打印原数组 Arrays.sort(arr); // 调用Arrays的sort()方法排序 System.out.print("排序后:"); printArray(arr); // 打印排序后数组 } }
使用Arrays的sort()方法时将会按照自然顺序对数组元素进行从小到大排序,使用非常方便。针对数组排序,数组工具类Arrays还提供了其他多个重载的sort()方法,既可以按照自然顺序进行排序,也可以传入比较器参数按照定制规则排序,同时还支持选择排序的元素范围。
2. 使用binarySearch()方法查找元素
在程序开发中,经常会在数组中查找某些特定的元素,如果数组中元素较多时查找某个元素,效率会非常低。为此,Arrays工具类中提供了一个binarySearch()方法用于查找元素。binarySearch()方法声明如下所示。
binarySearch(Object[] a, Object key);
内部是二分查找
import java.util.Arrays; public class Example25 { public static void main(String[] args) { int[] arr = { 9, 8, 3, 5, 2 }; Arrays.sort(arr); // 对数组进行排序 int index = Arrays.binarySearch(arr, 3); // 查找指定元素3 System.out.println("元素3的索引是:" + index); } }
3.使用copyOfRange()方法复制元素
在程序开发中,经常需要在不破坏原数组的情况下使用数组中的部分元素,这时可以使用Arrays工具类的copyOfRange()方法,copyOfRange()方法可以将数组中指定范围的元素复制到一个新的数组中。copyOfRange()方法声明格式如下所示。
copyOfRange(int[] original, int from,int to);
参数original表示被复制的数组,from表示被复制元素的初始索引(包括),to表示被复制元素的最后索引(不包括)
import java.util.Arrays; public class Example26 { public static void main(String[] args) { int[] arr = { 9, 8, 3, 5, 2 }; // 复制一个指定范围的数组 int[] copied = Arrays.copyOfRange(arr, 1, 7); for (int i = 0; i < copied.length; i++) { System.out.print(copied[i] + " "); } } }
4. 使用fill()方法替换元素
程序开发中,可能会需要将一个数组中的所有元素替换成同一个元素,此时可以使用Arrays工具类的fill()方法,该方法可以将指定的值赋给数组中的每一个元素,fill()方法声明如下所示。
fill(Object[] a,Object val);
参数a表示被修改的数组,val表示需要被替换成的元素
import java.util.Arrays; public class Example27 { public static void main(String[] args) { int[] arr = { 1, 2, 3, 4 }; Arrays.fill(arr, 8); // 用8替换数组中的每个元素 for (int i = 0; i < arr.length; i++) { System.out.println(i + ": " + arr[i]); } } }
Lambda表达式
Lambda表达式是JDK 8之后新增的一个新特性,Lambda可以取代大部分的匿名内部类,写出更优雅的Java代码,尤其在集合的遍历和其他集合操作中,可以极大地优化代码结构。JDK也提供了大量的内置函数式接口供我们使用,使得 Lambda 表达式的运用更加方便、高效。Lambda表达式由参数列表、箭头符号 -> 和方法体组成。方法体既可以是一个表达式,也可以是一个语句块。
import java.util.Arrays; public class Example28 { public static void main(String[] args) { String[] arr = {"program", "creek", "is", "a", "java", "site"}; Arrays.sort(arr, (m, n) -> Integer.compare(m.length(), n.length())); System.out.println("Lambda语句体中只有一条语句,参数类型可推断:"+ Arrays.toString(arr)); Arrays.sort(arr, (String m, String n) -> { if (m.length() > n.length()) return -1; else return 0; }); System.out.println("Lambda语句体中有多条语句:"+Arrays.toString(arr)); } }
IO
File类
File类的常用方法
import java.io.File; public class Example01 { public static void main(String[] args) { File f = new File("D:\\file\\a.txt"); //使用绝对路径创建File对象 File f1 = new File("src\\Hello.java"); //使用相对路径创建File对象 System.out.println(f); System.out.println(f1); } }
注意:在创建File对象时传入的路径使用了\\,这是因为Windows中的目录符号为反斜线\,但反斜线\在Java中是特殊字符,具有转义作用,所以使用反斜线\时,前面应该再添加一个反斜线,即为\\。此外,目录符号还可以用正斜线/表示,如“D:/file/a.txt”。
import java.io.File; public class Example02 { public static void main(String[] args) { File file = new File("src/test.txt"); System.out.println("文件是否存在:"+file.exists()); System.out.println("文件名:"+file.getName()); System.out.println("文件大小:"+file.length()+"bytes"); System.out.println("文件相对路径:"+file.getPath()); System.out.println("文件绝对路径:"+file.getAbsolutePath()); System.out.println("文件的父级对象是否为文件:"+file.isFile()); System.out.println("文件删除是否成功:"+file.delete()); } }
在一些特定情况下,程序需要读写一些临时文件,为此,File类提供了createTempFile()方法和deleteOnExit()方法,用于操作临时文件。createTempFile()方法用于创建一个临时文件, deleteOnExit()方法在JVM退出时自动删除临时文件。
import java.io.File; import java.io.IOException; public class Example03 { public static void main(String[] args) throws IOException { // 提供临时文件的前缀和后缀 File f = File.createTempFile("itcast-", ".txt"); f.deleteOnExit(); // JVM退出时自动删除文件f System.out.println("f是否为文件:"+f.isFile()); System.out.println("f的相对路径:"+f.getPath()); } }
遍历目录下的文件
1. 遍历指定目录下的所有文件
import java.io.File; public class Example04 { public static void main(String[] args) throws Exception { // 创建File对象 File file = new File("E:\\Java学科资料汇总(保密)\\Java学科\\18_《Java" +"基础入门》第三版教材\\3.案例文件\\chapter10\\src"); if (file.isDirectory()) { // 判断File对象对应的目录是否存在 String[] names = file.list (); // 获得目录下的所有文件的文件名 for (String name : names) { System.out.println(name); // 输出文件名 } } } }
2. 遍历指定目录下指定拓展名的文件
有时程序需要获取指定类型的文件,如获取指定目录下所有的“.java”文件。针对这种需求,File类提供了一个重载的list()方法,该方法接收一个FilenameFilter类型的参数。FilenameFilter是一个接口,被称作文件过滤器,其中定义了一个抽象方法accept()用于依次对指定File的所有子目录或文件进行迭代。在调用list()方法时,需要实现文件过滤器FilenameFilter,并在accept()方法中进行筛选,从而获得指定类型的文件。
// 创建File对象 File file = new File("E:\\Java学科资料汇总(保密)\\Java学科\\18_《Java" +"基础入门》第三版教材\\3.案例文件\\chapter10\\src"); // 创建过滤器对象 FilenameFilter filter = new FilenameFilter() { // 实现accept()方法 public boolean accept(File dir, String name) { File currFile = new File(dir, name); // 如果文件名以.java结尾返回true,否则返回false if (currFile.isFile() && name.endsWith(".java")) { return true; } else { return false; } } }; if (file.exists()) { // 判断File对象对应的目录是否存在 String[] lists = file.list(filter); // 获得过滤后的所有文件名数组 for (String name : lists) { System.out.println(name); } }
3. 遍历包括子目录文件的所有文件
有时候在一个目录下,除了文件还有子目录,如果想获取所有子目录下的文件,list()方法显然不能满足要求,这时可以使用File类提供的另一个方法listFiles()。listFiles()方法返回一个File对象数组,当对数组中的元素进行遍历时,如果元素中还有子目录需要遍历,则可以使用递归再次遍历子目录。
public class Example06 { public static void main(String[] args) { // 创建一个代表目录的File对象 File file = new File("E:\\Java学科资料汇总(保密)\\Java学科\\18_《Java" +"基础入门》第三版教材\\3.案例文件\\chapter10"); fileDir(file); // 调用FileDir方法 } public static void fileDir(File dir) { File[] files = dir.listFiles(); // 获得表示目录下所有文件的数组 for (File file : files) { // 遍历所有的子目录和文件 if (file.isDirectory()) { fileDir(file); // 如果是目录,递归调用fileDir() } System.out.println(file.getAbsolutePath()); // 输出文件的绝对路径 } } }
4. 删除文件及目录
在操作文件时,可能会遇到需要删除一个目录下的某个文件或者删除整个目录的情况,这时可以调用File类的delete()方法。
import java.io.*; public class Example07 { public static void main(String[] args) { File file = new File("D:\\hello"); if (file.exists()) { System.out.println(file.delete()); } } }
假如上面结果是false
说明文件删除失败了。原因是File类的delete()方法只能删除一个指定的文件,假如File对象代表目录,并且目录下包含子目录或文件,则File类的delete()方法不允许直接删除这个目录。在这种情况下,需要通过递归的方式将整个目录以及目录中的文件全部删除。
public static void deleteDir(File dir) { if (dir.exists()) { // 判断传入的File对象是否存在 File[] files = dir.listFiles(); // 得到File数组 for (File file : files) { // 遍历所有的子目录和文件 if (file.isDirectory()) { deleteDir(file); // 如果是目录,递归调用deleteDir() } else { // 如果是文件,直接删除 file.delete(); } } // 删除完一个目录里的所有文件后,就删除这个目录 dir.delete(); } } public class Example08 { public static void main(String[] args) { File file = new File("D:\\hello"); deleteDir(file); // 调用deleteDir删除方法 System.out.println("删除成功!"); } }
删除目录是从JVM直接删除而不放入回收站,文件一旦删除就无法恢复,因此在进行文件删除操作的时候需要格外小心。
字节流
在程序的开发中,经常需要处理设备之间的数据传输,而计算机中,无论是文本、图片、音频还是视频,所有文件都是以二进制(字节)形式存在的。对于字节的输入输出,I/O流提供了一系列的流,统称为字节流,字节流是程序中最常用的流,根据数据的传输方向可将其分为字节输入流和字节输出流。
抽象类InputStream和OutputStream
JDK提供了两个抽象类InputStream和OutputStream,它们是字节流的顶级父类,所有的字节输入流都继承自InputStream,所有的字节输出流都继承自OutputStream。为了方便理解,可以把InputStream和OutputStream比作两根“水管”,具体如下:
在左图中,InputStream被看成一个输入管道,OutputStream被看成一个输出管道,数据通过InputStream从源设备输入到程序,通过OutputStream从程序输出到目标设备,从而实现数据的传输。由此可见,I/O流中的输入输出都是相对于程序而言的。
InputStream类的常用方法
InputStream类虽然提供了一系列和读数据有关的方法,但是InputStream类是抽象类,不能被实例化,因此针对不同的功能,InputStream类提供了不同的子类,这些子类形成了一个体系结构。
OutputStream类的常用方法
OutputStream类虽然提供了一系列和写数据有关的方法,但是OutputStream类是抽象类,不能被实例化,因此针对不同的功能,OutputStream类提供了不同的子类,这些子类形成了一个体系结构。
字节流FileInputStream字节流读文件
InputStream就是JDK提供的基本输入流,它是所有输入流的父类,FileInputStream是InputStream的子类,它是操作文件的字节输入流,专门用于读取文件中的数据。
java根目录下有个文本文件test.txt,在文件中输入内容“itcast”并保存
public class Example09 { public static void main(String[] args) throws Exception { // 创建一个文件字节输入流,并指定源文件名称 FileInputStream in = new FileInputStream("test.txt"); int b = 0; // 定义一个int类型的变量b,记住每次读取的一个字节 while (true) { b = in.read(); // 变量b记住读取的一个字节 if (b == -1) { // 如果读取的字节为-1,跳出while循环 break; } System.out.println(b); // 否则将b写出 } in.close(); } }
控制台打印的结果分别为105、116、99、97、115和116。在test.txt文件中,字符‘i’‘t’‘c’‘a’‘s’‘t’各占一个字节,所以最终结果显示的就是文件test.txt中的六个字节所对应的十进制数。
文件读取发生错误处理方法
有时,在文件读取的过程中可能会发生错误。例如,文件不存在导致无法读取,或者用户没有读取权限等,这些错误都由JVM自动封装成IOException异常并抛出。如果文件读取过程中发生了IO错误,InputStream就无法正常关闭,资源也无法及时释放,这样会造成资源浪费。对此,可以使用try…finally保证InputStream在任何情况下都能够正确关闭。
public static void main(String[] args) throws Exception { InputStream input =null; try { // 创建一个文件字节输入流 FileInputStream in = new FileInputStream("test.txt"); int b = 0; // 定义一个int类型的变量b,记住每次读取的一个字节 while (true) { b = in.read(); // 变量b记住读取的一个字节 if (b == -1) { // 如果读取的字节为-1,跳出while循环 break; } System.out.println(b); // 否则将b写出 } } finally { if (input != null) { input.close(); } } }
字节流FileOutputStream字节流写文件
OutputStream是JDK提供的最基本的输出流,与InputStream类似,OutputStream也是抽象类,它是所有输出流的父类。OutputStream是一个抽象类,如果使用此类,则必须先通过子类实例化对象。OutputStream类有多个子类,其中FileOutputStream子类是操作文件的字节输出流,专门用于把数据写入文件。
import java.io.*; public class Example10{ public static void main(String[] args) throws Exception { // 创建一个文件字节输出流,并指定输出文件名称 OutputStream out = new FileOutputStream("example.txt"); String str = "传智教育"; byte[] b = str.getBytes(); for (int i = 0; i < b.length; i++) { out.write(b[i]); } out.close(); } }
使用FileOutputStream写数据时,程序自动创建了文件example.txt,并将数据写入example.txt文件。注意:如果通过FileOutputStream向一个已经存在的文件中写入数据,那么该文件中的数据会被覆盖。
在已存在的文件中追加内容
若希望在已存在的文件内容之后追加新内容,则可使用FileOutputStream的构造函数 FileOutputStream(String fileName, boolean append)创建文件输出流对象,并把append 参数的值设置为true。
public class Example11{ public static void main(String[] args) throws Exception { //创建文件输出流对象,并指定输出文件名称和开启文件内容追加功能 OutputStream out = new FileOutputStream("example.txt ", true); String str = "欢迎你!"; //将字符串存入byte类型的数组中 byte[] b = str.getBytes(); for (int i = 0; i < b.length; i++) { out.write(b[i]); } out.close(); } }
程序通过字节输出流对象out向文件example.txt写入“欢迎你!”后,并没有将文件之前的数据清空,而是将新写入的数据追加到了文件的末尾。
注意: I/O流在进行数据读写操作时会出现异常,为了代码的简洁,在InputStream读文件和OutputStream写文件的程序中都使用了throws关键字将异常抛出。然而一旦遇到IO异常,I/O流的close()方法将无法得到执行,流对象所占用的系统资源将得不到释放,因此,为了保证I/O流的close()方法必须执行,通常将关闭流的操作写在finally代码块中
文件的复制
在应用程序中,I/O流通常都是成对出现的,即输入流和输出流一起使用。例如,文件的复制就需要通过输入流读取文件中的数据,再通过输出流将数据写入文件。
// 创建一个字节输入流,用于读取当前目录下source文件夹中的a.png文件 InputStream in = new FileInputStream("source/a.png"); // 创建一个文件字节输出流,用于将读取的数据写入target目录下的文件中 OutputStream out = new FileOutputStream("target/b.png"); int len; // 定义一个int类型的变量len,记住每次读取的一个字节 // 获取复制文件前的系统时间 long begintime = System.currentTimeMillis(); while ((len = in.read()) != -1) { // 读取一个字节并判断是否读到文件末尾 out.write(len); // 将读到的字节写入文件 } // 获取文件复制结束时的系统时间 long endtime = System.currentTimeMillis(); System.out.println("复制文件所消耗的时间是:" + (endtime - begintime) + "毫秒"); in.close(); out.close();
上面的文件复制是一个字节一个字节的读写,需要频繁的操作文件,效率非常低。这就好比从北京运送烤鸭到上海,如果有一万只烤鸭,每次运送一只,就必须运输一万次,这样的效率显然非常低。为了减少运输次数,可以先把一批烤鸭装在车厢中,这样就可以成批的运送烤鸭,这时的车厢就相当于一个临时缓冲区。在通过流的方式复制文件时,为了提高效率也可以定义一个字节数组作为缓冲区。在复制文件时,可以一次性读取多个字节的数据,并保存在字节数组中,然后将字节数组中的数据一次性写入文件。程序中的缓冲区就是一块内存,该内存主要用于存放暂时输入输出的数据,由于使用缓冲区减少了对文件的操作次数,所以可以提高数据的读写效率。
// 创建一个字节输入流,用于读取当前目录下source文件夹中的文件a.png InputStream in = new FileInputStream("source/a.png"); // 创建一个文件字节输出流,用于将读取的数据写入当前目录的target文件中 OutputStream out = new FileOutputStream("target/a.png"); // 以下是用缓冲区读写文件 byte[] buff = new byte[1024]; // 定义一个字节数组,作为缓冲区 // 定义一个int类型的变量len记住读取读入缓冲区的字节数 int len; long begintime = System.currentTimeMillis(); while ((len = in.read(buff)) != -1) { // 判断是否读到文件末尾 out.write(buff, 0, len); // 从第一个字节开始,向文件写入len个字节 } long endtime = System.currentTimeMillis(); System.out.println("复制文件所消耗的时间是:" + (endtime - begintime) + "毫秒"); in.close(); out.close();
通过比较
可以看出使用缓冲区复制文件所消耗的时间明显减少了,这说明使用缓冲区读写文件可以有效的提高程序的读写效率。
BufferedInputStream
BufferedOutputStream
字符流
前面都是通过字节流直接对文件进行读写,如果读写的文件内容是字符,考虑到使用字节流读写字符可能存在传输效率以及数据编码问题,此时建议使用字符流。同字节流一样,字符流也有两个抽象的顶级父类,分别是Reader类和Writer类。其中Reader类是字符输入流,用于从某个源设备读取字符。Writer类是字符输出流,用于向某个目标设备写入字符。
Reader类的常用方法
Reader类作为字符流的顶级父类,也有许多子类,下面通过一张继承关系图列举Reader类的常用子类。
Writer类的常用方法
字符流FileReader字符流读文件
在程序开发中,经常需要对文本文件的内容进行读取,如果想从文件中直接读取字符便可以使用字符输入流FileReader,通过此流可以从关联的文件中读取一个或一组字符。
hapter10项目的根目录下新建文本文件src.txt并在文件中输入“hello itcast”; 其次在src文件夹中创建一个名称为Example16的类。
import java.io.*; public class Example16 { public static void main(String[] args) { // 定义源文件和目标文件路径 String srcFile = "src.txt"; String desFile = "des.txt"; try ( // 创建字节输入流读取src.txt文件 FileInputStream fis = new FileInputStream(srcFile); // 将字节输入流转换为字符输入流 InputStreamReader isr = new InputStreamReader(fis); // 创建字节输出流写入des.txt文件 FileOutputStream fos = new FileOutputStream(desFile); // 将字节输出流转换为字符输出流 OutputStreamWriter osw = new OutputStreamWriter(fos) ) { // 读取字符数据 char[] buffer = new char[1024]; int length; while ((length = isr.read(buffer)) != -1) { // 写入字符数据到目标文件 osw.write(buffer, 0, length); } System.out.println("文件复制完成!"); } catch (FileNotFoundException e) { System.err.println("文件未找到: " + e.getMessage()); } catch (IOException e) { System.err.println("IO异常: " + e.getMessage()); } } }
注意,不一定要转换,可以直接用字符流。
字符流FileWriter字符流写文件
在程序开发中,有时需要向文本文件写入内容,通过字符流向文本文件写入内容需要使用FileWriter类,FileWriter类可以一次向文件中写入一个或一组字符。
import java.io.*; public class Example15 { public static void main(String[] args) throws Exception { // 创建一个FileWriter对象用于向文件中写入数据 FileWriter writer = new FileWriter("writer.txt"); String str = "你好,传智教育"; writer.write(str); // 将字符数据写入到文本文件中 writer.write("\r\n"); // 将输出语句换行 writer.close(); // 关闭写入流,释放资源 } }
在已存在的文件中追加内容
FileWriter同FileOutputStream一样,如果指定的文件不存在,就会先创建文件,再写入数据,如果文件存在,则原文件内容会被覆盖。如果想在文件末尾追加数据,同样需要调用重载的构造方法。修改下,再次运行程序,即可实现在文件中追加内容的效果。
public class Example11{ public static void main(String[] args) throws Exception { //创建文件输出流对象,并指定输出文件名称和开启文件内容追加功能 FileWriter writer = new FileWriter("writer.txt",true); String str = "欢迎你!"; writer.write(str); out.close(); } }
BufferedReader
BufferedWriter
转换流
I/O流分为字节流和字符流,字节流和字符流之间可以进行转换。JDK提供了两个类用于将字节流转换为字符流,它们分别是InputStreamReader和OutputStreamWriter,也称为转换流,其作用如下所示。
(1)InputStreamReader是Reader的子类, 它可以将一个字节输入流转换成字符输入流,方便直接读取字符。 (2)OutputStreamWriter是Writer的子类, 它可以将一个字节输出流转换成字符输出流, 方便直接写入字符。
public static void main(String[] args) throws Exception { // 创建一个字节输入流in,并指定源文件为src.txt FileInputStream in = new FileInputStream("src.txt"); // 将字节输入流in转换成字符输入流isr InputStreamReader isr = new InputStreamReader(in); // 创建一个字节输出流对象out,并指定目标文件为des.txt FileOutputStream out = new FileOutputStream("des.txt"); // 将字节输出流out转换成字符输出流osw OutputStreamWriter osw = new OutputStreamWriter(out); int ch; // 定义一个变量用于记录读取的字符 while ((ch = isr.read()) != -1) { // 循环判断是否读取到文件的末尾 osw.write(ch); // 将字符数据写入des.txt文件中 } isr.close(); // 关闭字符输入流,释放资源 osw.close(); // 关闭字符输出流,释放资源 } }
序列化和反序列化
程序在运行过程中,数据都保存在Java中的对象中(内存),但很多情况下我们都需要将一些数据永久保存到磁盘上。为此,Java提供了对象序列化,对象序列化可以将对象中的数据保存到磁盘。 对象序列化(Serializable)是指将一个Java对象转换成一个I/O流的字节序列的过程。 反序列化(Deserialize)是指将I/O流中的字节序列恢复为Java对象的过程。
Serializable接口和Externalizable接口实现序列化机制的主要区别
对象实现支持序列化机制,这个对象所属的类必须是可序列化的。在Java中可序列化的类必须实现Serializable或Externalizable两个接口之一。
与实现Serializable接口相比,虽然实现Externalizable接口可以带来一定性能上的提升,但由于实现Externalizable接口,需要实现两个抽象方法,所以实现Externalizable接口也将导致编程的复杂度增加。在实际开发时,大部分都采用实现Serializable接口的方式来实现对象序列化。使用Serializable接口实现对象序列化非常简单,只需要让目标类实现Serializable接口即可,无需实现任何方法。
public class Person implements Serializable{ //为该类指定一个serialVersionUID变量值 private static final long serialVersionUID = 1L; //声明变量 private int id; private String name; private int age; // 此处省略各属性的getter和setter方法 ... }
serialVersionUID适用于Java的对象序列化机制。简单来说,Java的对象序列化机制是通过判断类的serialVersionUID来验证版本一致性。在进行反序列化时,JVM会把字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会抛出序列化版本不一致的异常。因此,为了在反序列化时确保序列化版本的兼容性,最好在每一个要序列化的类中加入private static final long serialVersionUID的变量值,具体数值可自定义,默认是1L。
注意
写操作,只有close之后能真正写入硬盘文件,或者flush也可以。