Java修仙,法力无边(光速回顾Java基础~)??ヽ(°▽°)ノ?
第一重 炼体(Java基础编程)
1.1 Java基础概述
Sun公司将Java划分为三个平台:JavaSE、JavaEE和JavaME
Java是一门编程语言,分为三大版本
三种Java版本之间的关系:
**JavaSE(标准版)**是JavaEE和JavaME的基础,用来开发C/S架构软件(即电脑桌面应用软件)
**JavaEE(企业版)**是在JavaSE的基础上构建的,用来开发B/S架构软件(企业级应用),在JavaSE的基础上进行了扩展,增加了一下更便捷的应用框架(比如现在常用的SpringMVC),我们可以应用这些框架轻松写出企业级的应用软件
**JavaME(微型版)**也是以JavaSE为基础的,它是一套专门为嵌入式设备设计的API接口规范,主要用于开发移动设备软件和嵌入式设备软件
Java是一门优秀的编程语言,其最主要的特点有以下几点:
1、简单:丢弃了C++中难以理解的运算符重载、多重继承等模糊的概念,特别是Java中不使用指针,而是使用引用,并提供了自动的垃圾回收机制,使程序员不必为内存管理而担忧
2、面向对象:提供了类、接口和继承等原语,为了简单起见,只支持类之间的单继承,但支持接口之间的多继承,并支持类与接口之间的实现机制(简而言之Java是一个纯粹的面向对象程序设计语言)
3、平台无关:Java语言编写的程序可以运行在各种平台之上,也就是说同一段程序既可以在Windows操作系统上运行,也可以在Linux操作系统上运行
4、多线程:所谓多线程可以简单理解为程序中有多个任务可以并发执行,这样可以在很大程度上提高程序的执行效率
5、动态:Java程序的 基本组成单元 就是 类 ,类又是运行时动态装载的,这使得Java可以在分布环境中动态地维护程序及类库
1.2 JDK简介
JDK:Sun公司提供了一套Java开发环境,简称JDK(Java Development Kit),它是整个Java的核心,其中包括Java编译器、Java运行工具、Java文档生成工具、Java打包工具等 (开发使用)
JRE:它是Java运行环境,是提供给普通用户使用的。由于用户只需要运行事先编写好的程序,不需要自己动手编写程序,因此JRE工具中只包含Java运行工具,不包含Java编译工具,SUN公司在其JDK工具中自带了一个JRE工具,也就是说开发环境中包含运行环境,这样一来,开发人员只需要在计算机上安装JDK即可,不需要专门安装JRE工具了
JDK安装目录下子目录介绍:
-
bin目录:该目录用于存放一些 可执行程序 ,如javac.exe(Java编译器)、java.exe(Java运行工具)、jar.exe(打包工具)和javadoc.exe(文档生成工具)等 -
db目录:db目录是一个小型的数据库。从JDK 6.0开始,Java中引入了一个新的成员JavaDB,这是 一个纯 Java 实现、开源的数据库管理系统 -
jre目录:“jre”是Java Runtime Environment的缩写,意为Java程序运行时环境。此目录是Java运行时环境的根目录,它包含Java虚拟机,运行时的类包、Java应用启动器以及一个bin目录,但不包含开发环境中的开发工具 -
include目录:由于JDK是通过C和C++实现的,因此在启动时需要引入一些C语言的头文件,该目录就是用于存放这些头文件的 -
lib目录:lib是library的缩写,意为Java类库或库文件,是开发工具使用的归档包文件 -
src.zip文件:src.zip为src文件夹的压缩文件,src中放置的是JDK核心类的源代码,通过该文件可以查看Java基础类的源代码
1.3 Java程序的开发
Java程序运行机制:
源程序(.java文件)-> Java编译器 -> 字节码(.class文件) -> 类装载器 -> 字节码校验器 -> 解释器 -> 操作系统平台
Java程序运行机制—虚拟机:
Java虚拟机可以理解成一个以 字节码为机器指令的CPU
对于不同的运行平台,有不同的虚拟机,实现了**“一 ”次编译,随处运行”**
Java虚拟机机制屏蔽了底层运行平台的差别
(1)Java程序运行时,经过编译和运行两个步骤,将.java的源文件进行编译,最终生成.class的字节码文件,然后由Java虚拟机将字节码文件进行解释执行,并将结果显示出来
(2)Java程序是由 虚拟机负责解释执行 的,而并非操作系统。这样做的好处是可以实现跨平台性,也就是说针对不同的操作系统可以编写相同的程序,只需安装不同版本的虚拟机即可
1.4 Java基本语法
Java程序代码必须放在一个 类 中,可以简单的把一个类理解为一个Java程序,类使用关键词class定义,class之前可以有类的修饰符
Java程序代码=结构定义语句(声明一个类或方法)+功能执行语句(实现具体功能)
1.4.1 Java中的变量
注意:
1、在Java中,一个小数会被默认为double类型的值,因此在为一个float类型的变量赋值时,在所赋值的后面一定要加上字母F(或者小写f),而为double类型的变量赋值时,可以在所赋值的后面加上字母D(或小写d),也可以不加
2、Java使用Unicode字符码系统,Unicode为每个字符制定了一个唯一的数值,在计算时,计算机会自动将字符转化为所对应的数值,如用97表示小写英文字母a
变量的类型转换:
1、隐式类型转换(自动类型转换):
指的是两种数据类型在转换的过程中不需要显式地进行声明,由编译器自动完成。自动类型转换必须同时满足两个条件,第一是两种数据类型彼此兼容,第二是目标类型的取值范围大于源类型的取值范围
3种可以进行自动类型转换的情况:
(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类型的变量
2、强制类型转换
显式类型转换,指的是两种数据类型之间的转换需要进行显式地声明。当两种类型彼此不兼容,或者目标类型取值范围小于源类型时,自动类型转换无法进行,这时就需要进行强制类型转换
强制类型转换格式:目标类型 变量 = (目标类型)值
**注意:**如果将取值范围较大的数据类型强制转换为取值范围较小的数据,如将一个int类型的数转为byte类型,极容易造成数据精度的丢失
1.4.2 Java中的运算符
算数运算符:
(1)自增自减时(++)(–)位于操作数的前后问题,先自增还是先进行其他运算
(2)除法运算中,整数之间相除会忽略小数部分
(3)在进行取模(%)运算时,运算结果的正负取决于被模数(%左边的数)的符号,与模数(%右边的数)的符号无关。例如,(-5)%3=-2,而5%(-3)=2
赋值运算符:
Java中可以通过一条赋值语句对多个变量进行赋值
合法赋值语句:int x,y,z; x=y=z=5;
非法赋值语句:int x=y=z=5;
逻辑运算符:
(1)在使用“&”进行运算时,不论左边为true或者false,右边的表达式都会进行运算。在使用“&&”进行运算,当左边为false时,右边的表达式就不再进行运算,因此“&&”被称作短路与
(2)运算符“|”和“||”都表示或操作,当运算符两边的任一表达式值为true时,其结果为true。只有两边表达式的值都为false时,其结果才为false,“||”运算符为短路或,当运算符“||”的左边为true时,右边的表达式不再进行运算
(3)运算符“^”表示异或操作,当运算符两边的布尔值相同时(都为true或都为false),其结果为false。当两边表达式的布尔值不相同时,其结果为true(同为false异为true)
1.5 方法
定义:方法就是一段可以重复调用的代码,有些书中也会把方法称为函数
语法格式:
修饰符 返回值类型 方法名(参数类型 参数名1,参数类型 参数名2,...){ 执行语句 … return 返回值; }
方法的重载:参数不同的方法有着相同的名字,调用时根据参数不同确定调用哪个方法,这就是Java方法重载机制
注意:通过传入不同的参数便可以确定调用哪个重载的方法,方法的重载与返回值类型无关
1.6 数组
定义:数组是指一组类型相同的数据的集合,数组中的每个数据被称作元素(可存放任意类型的元素,但同一数组里存放的元素类型必须一致)
Java中数组的声明方式:
- 数据类型[] 数组名 = null;
- 数据类型[]数组名;
数组名= new数据类型[长度];
注意:若数组不进行初始化,系统默认初始化,不同类型数组元素的默认值
二维数组的定义
第一种方式: 数据类型[][] 数组名 = new数据类型【行数】【列数】;
第二种方式: 数据类型[][] 数组名 = new int【行的个数】【】;
第三种方式: 数据类型[][] 数组名= {{第0行初始值},{第1行初始值},…,{第n行初始值}}; int[][] xx= {{1,2},{3,4,5,6},{7,8,9}};
1.7 炼体阶段案例
【案例1—Product information management system】
介绍:通过数组对产品信息完成初始化,用户后期可通过“Update”功能对原信息进行更新覆盖,也可以通过产品名称查询已录入的或初始化的产品信息,也可以查询基本统计信息
package package_01;
import java.util.Scanner;
public class Class_01 {
static String[] names = {"iPhone","HuaiWei","Vivo"};
static int[] price = {8900,7400,4500};
static int[] number = {200,19,120,50,230};
private static void menu() {
System.out.println("-- Management Information System --");
System.out.println("------------------------------------");
System.out.println("------------ 1.Show --------------");
System.out.println("------------ 2.Update ------------");
System.out.println("------------ 3.Inquire ------------");
System.out.println("------------ 4.Statistics --------");
System.out.println("------------ 5.Exist -------------");
System.out.println("----------------------------");
System.out.println("->Please choose your operation:");
}
public static void main(String[] arge) {
Scanner input = new Scanner(System.in);
outer:
while(true) {
menu();
int c = input.nextInt();
switch(c) {
case 1:function1();
break;
case 2:function2();
break;
case 3:function3();
break;
case 4:function4();
break;
case 5:break outer;
}
}
}
private static void function1() {
for(int i=0;i<names.length;i++) {
String n=names[i];
int p=price[i];
int b=number[i];
System.out.print((i+1)+" . "+"名称:"+n+"\t价格:"+p+"\t库存:"+b+"\n");
System.out.print("------------------------------------------");
System.out.print("\n");
}
}
private static void function2() {
Scanner input = new Scanner(System.in);
System.out.print("Start the Update Operation:");
for(int i=0;i<names.length;i++) {
System.out.print("Please input the name:");
String n = input.next();
names[i]=n;
System.out.print("Please input the price:");
int p = input.nextInt();
price[i]=p;
System.out.print("Please input the number:");
int b = input.nextInt();
number[i]=b;
}
function1();
}
private static void function3() {
Scanner input = new Scanner(System.in);
System.out.print("--产品信息查询--");
System.out.print("请输入要查询的产品名称:");
String x = input.next();
for(int i=0;i<names.length;i++) {
if(x.equals(names[i])) {
String n = names[i];
int p = price[i];
int b = number[i];
System.out.print("-> 名称:"+n+"\t价格:"+p+"\t库存:"+b+"\n");
return ;
}
}
System.out.print("查无此物!");
}
private static void function4() {
int sum=0;
int anverage=0;
int highestSum=0;
int highestOne=0;
for (int i = 0; i < names.length; i++) {
sum+=price[i]*number[i];
anverage+=price[i];
if (price[i]*number[i]>highestSum) {
highestSum=price[i]*number[i];
}
if (price[i]>highestOne) {
highestOne=price[i];
}
}
System.out.println("商品总价:"+sum);
System.out.println("单价均价:"+(anverage/names.length));
System.out.println("最高总价:"+highestSum);
System.out.println("最高单价:"+highestOne);
}
}
【案例2—Shopping In Supermarket】
package package_01;
import java.util.Scanner;
public class Class_02 {
static int price[]= {3,5,2,7};
static int length=10;
static int[] price_sum=new int[length];
public static void menu() {
System.out.println("---- Please choose your fruit ----");
System.out.println("-----------------------------------");
System.out.println("--------- 1.apple 3$ -----------");
System.out.println("--------- 2.banana 5$ ----------");
System.out.println("--------- 3.pear 2$ --------------");
System.out.println("--------- 4.watermelon 7$ ------");
System.out.println("------------ 5.Exist ------------");
System.out.println("-----------------------------------");
System.out.println("->Please choose your operation:");
}
public static void main(String [] arge) {
Scanner input = new Scanner(System.in);
int i=0;
int sum=0;
outer:
while(true) {
System.out.print("Please input your choice:");
int m = input.nextInt();
System.out.print("Do you want to get more?(choose 'Y'or'N')");
price_sum[i]=m-1;
++i;
String s = input.next();
if(s.equals("Y")) continue;
else if(s.equals("N")) break outer;
else continue;
}
System.out.print("Your shopping is over!");
for(int j=0;j<i;j++) {
sum+=price[price_sum[j]];
}
System.out.print("The sum of your choice:"+sum);
}
【案例3—Distribution Department】
package package_01;
import java.util.Scanner;
public class Class_03 {
static String[] major= {"Java","C#","asp.net","Front end"};
public static void main(String []arge) {
Scanner input = new Scanner(System.in);
outer:
while(true) {
System.out.print("Please enter your major:");
String m = input.next();
for(int i=0;i<major.length;i++) {
if(m.equals(major[i])) {
System.out.print("Congratulation on jioning "+major[i]+" department!");
break outer;
}
}
System.out.print("We don't have this department! please check your input and try again!");
}
}
}
【案例4—Rock-paper-scissors】
package stjdb;
import java.util.Scanner;
import java.util.Random;
public class Class_04 {
public static void main(String[] args) {
System.out.print("-----------------------------------\n");
System.out.print("--- 1=Rock 2=paper 3=scissors ----\n");
System.out.print("-----------------------------------\n");
Random rand = new Random();
Scanner input = new Scanner(System.in);
int r=0;
int c=0;
outer:
while(true) {
for(int i=0;i<5;i++) {
int s = rand.nextInt(5)+1;
System.out.print("--The system has its choice!\n");
System.out.print("--Please enter your choice:");
int m = input.nextInt();
if(m!=1 && m!=2 && m!=3) {
System.out.print("\nyour input is wrong! please check your input and try again!\n");
continue;
}
else {
if(m==(s+1) || m==(s-2)){
r+=1;
System.out.print("your victory +1!\n");
System.out.print("The number of people's victory = "+r+"\n");
}
else {
c++;
System.out.print("computer's victory +1!\n");
System.out.print("The number of computer's victory = "+c+"\n");
}
}
System.out.print("-----------------------------------\n");
}
if(r==5) {
System.out.print("you victory!");
break outer;
}
else if(c==5) {
System.out.print("computer victory!");
break outer;
}
else continue;
}
}
}
【案例5—Login And Registration】
package stjdb;
import java.util.Scanner;
public class Class_05 {
static double[] user_name = new double[10];
static double[] pass_word = new double[10];
public static void menu() {
System.out.println("-----------------------------------------------");
System.out.println("-- Bank User Information Management System --");
System.out.println("-----------------------------------------------");
System.out.println("------------ 1.Login ------------------------");
System.out.println("------------ 2.Registration -----------------");
System.out.println("------------ 3.Show --------------------------");
System.out.println("------------ 4.Change password --------------");
System.out.println("------------ 5.Exist ------------------------");
System.out.println("-----------------------------------------------");
System.out.println("->Please choose your operation:");
}
public static void function1() {
Scanner input = new Scanner(System.in);
System.out.println("Please enter your user name:");
double un = input.nextDouble();
while(true) {
outer:
for(int i=0;i<user_name.length;i++) {
if(un==user_name[i]) {
System.out.println("Please enter your password:");
double pw = input.nextDouble();
for(int j=0;j<pass_word.length;j++) {
if(pw==pass_word[j]) {
System.out.println("Login Succeeded!");
return;
}
}
System.out.println("Login Failed! Error:your password is wrong!");
}
}
System.out.println("Your user name is not exist!");
return;
}
}
public static void function2() {
Scanner input = new Scanner(System.in);
int x=1;
for(int i=0;i<user_name.length;i++) {
if(user_name[i]==0) {
break;
}
x++;
}
System.out.println("You are the No."+x+" user!");
outer:
while(true) {
System.out.println("Please input your user name:");
double un_new = input.nextDouble();
for(int j=0;j<user_name.length;j++) {
if(user_name[j]==un_new) {
System.out.println("The user name is existed! please choose an another one!");
continue outer;
}
else {
user_name[x-1]=un_new;
System.out.println("Please input your pass:");
double pw_new = input.nextDouble();
pass_word[x-1]=pw_new;
break outer;
}
}
}
}
public static void function3() {
System.out.println("The information of all user:");
for(int i=0;i<user_name.length;i++) {
if(user_name[i]!=0) {
System.out.println("User name:"+user_name[i]+"\tPass word:"+pass_word[i]);
}
else break;
}
}
public static void function4() {
System.out.println("Please input the user of the one you want to change its password");
Scanner input = new Scanner(System.in);
double order = input.nextDouble();
outer:
while(true) {
for(int i=0;i<user_name.length;i++) {
if(user_name[i]==order) {
System.out.println("Please input your new password:");
double ps_new = input.nextDouble();
pass_word[i]=ps_new;
System.out.println("Change succeeded!");
break outer;
}
}
System.out.println("The user is not existed!");
}
}
public static void main(String[] args) {
user_name[0]=1981885492;
pass_word[0]=123456;
Scanner input = new Scanner(System.in);
outer:
while(true) {
menu();
int a = input.nextInt();
switch(a) {
case 1:function1();
break;
case 2:
function2();
break;
case 3:
function3();
break;
case 4:
function4();
break;
case 5:
break outer;
}
}
}
}
第二重 筑基(面向对象思想)
2.1 面向对象思想
面向对象:把构成问题的事务按照一定规则划分为多个独立的对象,然后通过调用对象的方法来解决问题,在程序中使用对象映射现实中的事物,使用对象的关系描述事物之间的联系,这种思想就是面向对象
面向对象的三大特性:封装、继承、多态
(1)封装性(面向对象的核心思想):
- 第一层含义指把对象的属性和行为看成是一个密不可分的整体,将这两者“封装”在一起(即封装在对象中)
- 另外一层含义指“信息隐藏”,将不想让外界知道的信息隐藏起来。例如,驾校的学员学开车,只需要知道如何操作汽车,无需知道汽车内部是如何工作的
(2)继承性:继承性主要描述的是类与类之间的关系,通过继承,可以在无需重新编写原有类的情况下,对原有类的功能进行扩展,继承不仅增强了代码的复用性、提高开发效率,还降低了程序产生错误的可能性,为程序的维护以及扩展提供了便利
(3)多态:指的是在一个类中定义的属性和方法被其它类继承后,它们可以具有不同的数据类型或表现出不同的行为,这使得同一个属性和方法在不同的类中具有不同的语义
2.2 类与对象
类与对象:为了做到让程序对事物的描述与事物在现实中的形态保持一致,面向对象思想中提出了两个概念,即类和对象,在Java程序中类和对象是最基本、最重要的单元。类表示某类群体的一些基本特征抽象,对象表示一个个具体的事物
类用于描述多个对象的共同特征,它是对象的模板。对象用于描述现实中的个体,它是类的实例。对象是根据类创建的,一个类可以对应多个对象
面向对象思想与类的关系:面向对象的思想中最核心的就是对象,而创建对象的前提是需要定义一个类,类是Java中一个重要的引用数据类型,也是组成Java程序的基本要素,所有的Java程序都是基于类的
1.类的定义
类中可以定义成员变量和成员方法,其中,成员变量用于描述对象的特征,成员变量也被称作对象的属性;成员方法用于描述对象的行为,可简称为方法
(特征 -> 属性 / 行为 -> 方法)
成员变量与局部变量::定义在类中的变量被称为成员变量,定义在方法中的变量被称为局部变量。如果在某一个方法中定义的局部变量与成员变量同名,这种情况是允许的,此时,在方法中通过变量名访问到的是局部变量,而并非成员变量(成员变量在类中,局部变量在类的方法会中)
2.对象的创建及使用(对象是类的实例,对象是类的模板)
基本语法:类名 对象名称 = new 类名();
对象的内存分配:使用new关键字创建的对象是在堆内存分配空间
类是引用数据类型:引用数据类型就是指内存空间可以同时被多个栈内存引用
3.对象的引用传递
class Student {
String name;
int age;
void read() {
System.out.println("大家好,我是"+name+",年龄"+age);
}
}
class Example02 {
public static void main(String[] args) {
Student stu1 = new Student ();
Student stu2 = null;
stu2 = stu1;
stu1.name = "小明";
stu1.age = 20;
stu2.age = 50;
stu1.read();
stu2.read();
}
}
对象的引用传递的实质:
首先声明stu1并为stu1分配内存空间,然后stu1对象为stu2对象分配使用权,stu1和stu2指向同一内存(引用的实质)
4.访问控制
针对类、成员方法和属性,Java提供了4种访问控制权限,分别是private、default、protected和public,这4种访问控制权限按级别由小到大依次排列
(1)private(当前类访问级别):属于私有访问权限,用于修饰类的属性和方法,类的成员一旦使用了private的访问权限,则该类成员只能在本类中进行访问
(2)default:如果一个类中的属性或方法没有进行任何的访问权限声明,则默认为default的访问权限,默认的访问权限可以被本包中的其他类访问,但是不能被其他包中的类访问
(3)protected:属于受保护的访问权限,一个类中的成员若使用了protected访问权限,则只能被本包及不同包的子类访问
(4)public:属于公共访问权限,若使用了该访问权限,则该成员可以在所有类中被访问,不管是否在同一包中
2.3 封装
封装:封装是指一种将抽象性函数式接口的实现细节部分包装、隐藏起来的方法。封装可以被认为是一个保护屏障,防止本类的代码和数据被外部类定义的代码随机访问
Java开发中,在定义一个类时,将类中的属性私有化,即使用private关键字修饰类的属性,被私有化的属性只能在类中被访问。如果外界想要访问私有属性,则必须通过setter和getter方法设置和获取属性值
2.4 构造方法
构造方法:实例化一个对象后,如果要为这个对象中的属性赋值,则必须通过直接访问对象的属性或调用setter方法才可以,如果需要在实例化对象时为这个对象的属性赋值,可以通过构造方法实现
构造方法(也被成为构造器)是类的一个特殊成员方法,在类实例化对象时自动调用
构造方法是一个特殊的成员方法,在定义时,有以下几点需要注意: (1)构造方法的名称必须与类名一致。 (2)构造方法名称前不能有任何返回值类型的声明。 (3)不能在构造方法中使用return返回一个值,但是可以单独写return语句作为方法的结束
无参构造方法与有参构造方法:
- 无参构造方法:
1 class Student{
2 public Student() {
3 System.out.println("调用了无参构造方法");
4 }
5 }
6 public 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 }
- 有参构造方法:
class Student{
private String name;
private int age;
public Student(String n, int a) 5{
name = n;
age = a;
}
public void read(){
System.out.println("我是:"+name+",年龄:"+age);
}
}
public class Example06 {
public static void main(String[] args) {
Student stu = new Student("张三",18);
stu.read();
}
}
构造方法的重载:
与普通方法一样,构造方法也可以重载,在一个类中可以定义多个构造方法,只要每个构造方法的参数或参数个数不同即可。在创建对象时,可以通过调用不同的构造方法为不同的属性赋值
class Student{
private String name;
private int age;
public Student() { }
public Student(String n) {
name = n;
}
public Student(String n, int a) {
name = n;
age = a;
}
public void read(){
System.out.println("我是:"+name+",年龄:"+age);
}
}
public class Example07 {
public static void main(String[] args) {
Student stu1 = new Student("张三");
Student stu2 = new Student("张三",18);
stu1.read();
stu2.read();
}
}
需要注意的是,构造方法通常使用public进行修饰
2.5 this关键字与static关键字
1.this关键字
当成员变量与局部变量发生重名问题时,需要使用到this关键字分辨成员变量与局部变量(有参构造函数就是一个典型的例子)
参数名称与对象成员变量名称相同,编译器无法确定哪个名称是当前对象的属性
Java中的this关键字语法比较灵活,其主要作用主要有以下3种。 (1)使用this关键字调用本类中的属性 (2)this关键字调用成员方法 (3)使用this关键字调用本类的构造方法
构造方法是在实例化对象时被Java虚拟机自动调用,在程序中不能像调用其他成员方法一样调用构造方法,但可以在一个构造方法中使用“this(参数1,参数2…)”的形式调用其他的构造方法
class Student {
private String name;
private int age;
public Student () {
System.out.println("实例化了一个新的Student对象。");
}
public Student (String name,int age) {
this();
this.name = name;
this.age = age;
}
public String read(){
return "我是:"+name+",年龄:"+age;
}
}
public class Example11 {
public static void main(String[] args) {
Student stu = new Student ("张三",18);
System.out.println(stu.read());
}
}
注意:在使用this调用类的构造方法时,应注意以下几点
(1)只能在构造方法中使用this调用其他的构造方法,不能在成员方法中通过this调用其他构造方法
(2)在构造方法中,使用this调用构造方法的语句必须位于第一行,且只能出现一次。下面程序的写法是错误的
(3)不能在一个类的两个构造方法中使用this互相调用
2.static关键字
在定义一个类时,只是在描述某事物的特征和行为,并没有产生具体的数据。只有通过new关键字创建该类的实例对象时,才会开辟栈内存及堆内存,在堆内存中要保存对象的属性时,每个对象会有自己的属性。如果希望某些属性被所有对象共享,就必须将其声明为static属性。如果属性使用了static关键字进行修饰,则该属性可以直接使用类名称进行调用
static可以修饰属性和方法(被修饰后分别称为静态属性和静态方法)
(1)静态属性:
在Java程序中使用static修饰属性,则该属性称为静态属性(也称全局属性),静态属性可以使用类名直接访问,访问格式如下: 类名.属性名
只需改变一个对象的静态属性,所有对象的此属性都会被修改
(2)静态方法:
同静态变量一样,静态方法也可以通过类名和对象访问,具体如下所示。 类名.方法 或 实例对象名.方法
class Student {
private String name;
private int age;
private static String school = "A大学";
public Student(String name,int age){
this.name = name;
this.age = age;
}
public void info(){
System.out.println("姓名:" +this.name+",年龄:" + this.age+",学校:" + school);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public static String getSchool() {
return school;
}
public static void setSchool(String school) {
Student.school = school;
}
}
class Example15 {
public static void main(String[] args) {
Student stu1 = new Student("张三",18);
Student stu2 = new Student("李四",19);
Student stu3 = new Student("王五",20);
stu1.setAge(20);
stu2.setName("小明");
Student.setSchool("B大学");
stu1.info();
stu2.info();
stu3.info();
}
}
注意:静态方法只能访问静态成员,因为非静态成员需要先创建对象才能访问,即随着对象的创建,非静态成员才会分配内存,而静态方法在被调用时可以不创建任何对象
2.6 代码块
代码块,简单来讲,就是用“{}”括号括起来的一段代码,根据位置及声明关键字的不同,代码块可以分为4种:普通代码块、构造块、静态代码块、同步代码块
(1)普通代码块就是直接在方法或是语句中定义的代码块
(2)构造代码块就是直接在类中定义的代码块
class Student{
String name;
{
System.out.println("我是构造代码块");
}
public Student(){
System.out.println("我是Student类的构造方法");
}
}
public class Example12 {
public static void main(String[] args) 13{
Student stu1 = new Student();
Student stu2 = new Student();
}
}
注意:在实例化Student类的对象stu1和stu2时,构造块的执行顺序大于构造方法(与构造块和构造方法在类中的位置没有关系),每当实例化一个类时,都会在执行构造方法之间执行构造块
(3)静态代码块
用static关键字修饰的代码块称为静态代码块。当类被加载时,静态代码块会执行,由于类只加载一次,因此静态代码块只执行一次。在程序中,通常使用静态代码块对类的成员变量进行初始化
【案例3-4 学生投票系统】
package stjdb;
import java.util.Scanner;
public class Class_06 {
static int[] book_price= {15,23,18,7,32};
static int[] book_inventory= {2,1,0,3,2};
static String[] book_name = {"Science","Natural","Geography","Action","Suspense"};
class Book{
String name;
double price;
int inventory;
public Book(String name,double price,int inventory) {
this.name=name;
this.price=price;
this.inventory=inventory;
}
}
public static void menu() {
System.out.println("------------------------------------------------------------------------");
System.out.println("------------------------ Book Purchase System ------------------------");
System.out.println("------------------------------------------------------------------------");
System.out.println("-----------| Name | Price | Inventory |------");
System.out.println("------------------------------------------------------------------------");
System.out.println("-----------| 1.Science | 15 | 2 |------");
System.out.println("-----------| 2.Natural | 23 | 1 |------");
System.out.println("-----------| 3.Geography | 18 | 0 |------");
System.out.println("-----------| 4.Action | 7 | 3 |------");
System.out.println("-----------| 5.Suspense | 32 | 2 |------");
System.out.println("------------------------------------------------------------------------");
System.out.println("->Start your shopping , please input your choice:");
}
public class Example {
Book book_01 = new Book("Science",15,100);
Book book_02 = new Book("Natural",15,100);
Book book_03 = new Book("Geography",15,100);
Book book_04 = new Book("Action",15,100);
Book book_05 = new Book("Suspense",15,100);
}
public static void Main(String[] args) {
Scanner input = new Scanner(System.in);
outer1:
while(true) {
menu();
int sum=0;
String order = input.next();
outer2:
for(int i=0;i<book_name.length;i++) {
if(order==book_name[i] && book_inventory[i]>0) {
System.out.println("Are you sure to add "+book_name[i]+" to the shopping cart? (YES or NO)");
String select = input.next();
if(select=="YES") {
sum+=book_price[i];
System.out.println("Purchase successful ! Your total: "+sum);
System.out.println("Do you want to continue your shopping ? (YES or NO)");
String select2 = input.next();
if(select2=="YES") {
continue outer1;
}
else {
break outer1;
}
}
else if(select == "NO") {
System.out.println("Cancellation succeeded !");
continue outer1;
}
else {
System.out.println("Your input is wrong!");
break outer2;
}
}
}
}
}
}
2.7 类的继承
在程序中,继承描述的是事物之间的所属关系,通过继承可以使多种事物之间形成一种关系体系
定义:在Java中,类的继承是指在一个现有类的基础上去构建一个新的类,构建出来的新类被称作子类,现有类被称作父类。子类继承父类的属性和方法,使得子类对象(实例)具有父类的特征和行为
在继承中需要注意的点:
(1)在Java中,类只支持单继承,不允许多重继承。也就是说一个类只能有一个直接父类,例如下面这种情况是不合法的
(2)多个类可以继承一个父类
(3)在Java中,多层继承也是可以的,即一个类的父类可以再继承另外的父类。例如,C类继承自B类,而B类又可以继承自A类,这时,C类也可称作A类的子类。例如下面这种情况是允许的
(4)在Java中,子类和父类是一种相对概念,一个类可以是某个类的父类,也可以是另一个类的子类
方法的重写:在继承关系中,子类会自动继承父类中定义的方法,但有时在子类中需要对继承的方法进行一些修改,即对父类的方法进行重写
注意:在子类中重写的方法需要和父类被重写的方法具有相同的方法名、参数列表以及返回值类型,且在子类重写的方法不能拥有比父类方法更加严格的访问权限
super关键字
当子类重写父类的方法后,子类对象将无法访问父类被重写的方法,为了解决这个问题,Java提供了super关键字,super关键字可以在子类中调用父类的普通属性、方法以及构造方法
super关键字的几种用法:
(1)使用super关键字访问父类成员变量和方法:
语法:super.成员方法(参数1,参数2…)
class Animal {
String name = "牧羊犬";
void shout() {
System.out.println("动物发出叫声");
}
}
class Dog extends Animal {
public void shout() {
super.shout();
System.out.println("汪汪汪……");
}
public void printName(){
System.out.println("名字:"+super.name);
}
}
public class Example05 {
public static void main(String[] args) {
Dog dog = new Dog();
dog.shout();
dog.printName();
}
}
(2)使用super关键字方法父类中指定的构造方法
语法:super(参数1,参数2…)
class Animal {
private String name;
private int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
21 public String info() {
return "名称:"+this.getName()+",年龄: "+this.getAge();
}
}
class Dog extends Animal {
private String color;
public Dog(String name, int age, String color) {
super(name, age);
this.setColor(color);
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public String info() {
return super.info()+",颜色:"+this.getColor();
}
}
public class Example06 {
public static void main(String[] args) {
Dog dog = new Dog("牧羊犬",3,"黑色");
System.out.println(dog.info());
}
}
使用super(name,age);
继承了父类中的 this.name=name; 和 this.age=age;
并且可以在super();语句之后添加拓展的构造语句
注意:通过super()调用父类构造方法的代码必须位于子类构造方法的第一行,并且只能出现一次
2.8 final关键字
可以使用final关键字声明类、属性、方法,在声明时需要注意以下几点: (1)使用final修饰的类不能有子类 (2)使用final修饰的方法不能被子类重写 (3)使用final修饰的变量(成员变量和局部变量)是常量,常量不可修改
final关键字是一种限制和修饰,可以让一个类不能有子类,且如果父类的方法被final关键字修饰,那么它就不能再被子类重写该方法,被其修饰的变量称为常量,其值不能再发生改变
注意:在使用final声明变量时,要求全部的字母大写。如果一个程序中的变量使用public static final声明,则此变量将成为全局变量,如下面代码所示。
public static final String NAME = "哈士奇";
2.9 抽象类与接口
当定义一个类时,常常需要定义一些成员方法描述类的行为特征,但有时这些方法的实现方式是无法确定的
抽象方法:抽象方法用abstract关键字修饰的方法,抽象方法在定义时不需要实现方法体
语法:abstract void 方法名称(参数);
注意:当一个类包含了抽象方法,该类必须是抽象类。抽象类和抽象方法一样,必须使用abstract关键字进行修饰
abstract class 抽象类名称{
访问权限 返回值类型 方法名称(参数){
return [返回值];
}
访问权限 abstract 返回值类型 抽象方法名称(参数);
}
抽象类的定义规则如下: (1)包含一个以上抽象方法的类必须是抽象类 (2)抽象类和抽象方法都要使用abstract关键字声明 (3)抽象方法只需声明而不需要实现 (4)如果一个类继承了抽象类,那么该子类必须实现抽象类中的全部抽象方法(接口)
使用abstract关键字修饰的抽象方法不能使用private修饰,因为抽象方法必须被子类实现,如果使用了private声明,则子类无法实现该方法
接口:如果一个抽象类的所有方法都是抽象的,则可以将这个类定义接口。接口是Java中最重要的概念之一,接口(并不是一个独立的东西)是一种特殊的类,由全局常量和公共的抽象方法组成,不能包含普通方法
JDK 8对接口进行了重新定义,接口中除了抽象方法外,还可以有默认方法和静态方法(也叫类方法),默认方法使用default修饰,静态方法使用static修饰,且这两种方法都允许有方法体
接口的声明:使用interface关键字声明
public interface 接口名 extends 接口1,接口2... {
public static final 数据类型 常量名 = 常量值;
public default 返回值类型 抽象方法名(参数列表);
public abstract 返回值类型 方法名(参数列表){
}
public abstract 返回值类型方法名(参数列表){
}
}
注意: 接口中的变量默认使用“public static final”进行修饰,即全局常量。接口中定义的方法默认使用“public abstract”进行修饰,即抽象方法。如果接口声明为public,则接口中的变量和方法全部为public
“extends 接口1,接口2…”表示一个接口可以有多个父接口,父接口之间使用逗号分隔。Java使用接口的目的是为了克服单继承的限制,因为一个类只能有一个父类,而一个接口可以同时继承多个父接口
多学一点:经常看到编写接口中的方法时省略了public ,但是接口中的方法访问权限永远是public。与此类似,在接口中定义常量时,可以省略前面的“public static final”,此时,接口会默认为常量添加“public static final”
接口的使用: 接口的使用必须通过子类,子类通过implements关键字实现接口,并且子类必须实现接口中的所有抽象方法。需要注意的是,一个类可以同时实现多个接口,多个接口之间需要使用英文逗号(,)分隔
修饰符 class 类名 implements 接口1,接口2,...{
...
}
接口实例:
interface Animal{
int ID = 1;
String NAME = "牧羊犬";
void shout();
static int getID(){
return ID;
}
public void info();
}
interface Action{
public void eat();
}
class Dog implements Animal,Action{
public void eat(){
System.out.println("骨头");
}
public void shout(){
System.out.println("汪汪汪");
}
public void info(){
System.out.println("名称"+NAME);
}
}
class Example11{
public ststic void main(String args[]){
Systrm.out.println("编号"+Animal.getID());
Dog dog = new Dog();
dog.info();
dog.shout();
dog.eat();
}
}
注意:接口的实现类,必须实现接口中的所有方法,否则程序编译报错!
如果在开发中一个子类既要实现接口又要继承抽象类,则可以按照以下格式定义子类:
修饰符class 类名 extends 父类名implements 接口1,接口2,... {
...
}
2.10 多态
多态性是面向对象思想中的一个非常重要的概念,在Java中,多态是指不同对象在调用同一个方法时表现出的多种不同行为,在同一个方法中,这种由于参数类型不同而导致执行效果不同的现象就是多态
多态的两种主要形式:(1)方法的重载;(2)对象的多态性——方法重写
对象类型的转换:(1)向上转型:子类–>父类;(2)向下转型:父类–>子类
转换格式:
- 对象向上转型: 父类类型 父类对象 = 子类实例;
- 对象向下转型:
父类类型 父类对象 = 子类实例;
子类类型 子类对象 = (子类)父类对象;
向上转型实例:
class Animal{
public void shout(){
System.out.println("喵喵...");
}
}
class Dog extends Animal{
public void shout(){
System.out.println("汪汪...");
}
}
public class Example15{
public static void main(String args[]){
Dog dog = new Dog();
Animal an = dog;
an.shout();
}
}
注意:如果对象发生了向上转型关系后,所调用的方法一定是被子类重写过的方法(即其本质不变)
父类Animal的对象an是无法调用Dog类中的eat()方法的,因为eat()方法只在子类中定义,而没有在父类中定义
向下转型实例:
class Animal{
public void shout(){
System.out.println("喵喵...");
}
}
class Dog extends Animal{
public void shout(){
System.out.println("汪汪...");
}
public void eat(){
System.out.println("吃骨头");
}
}
public class Example16{
public static void main(String args[]){
Animal an = new Dog();
Dog dog = (Dog)an;
dog.shout();
dog.eat();
}
}
在向下转型时,不能直接将父类实例强制转换为子类实例,否则程序会报错,必须首先用父类创建一个子类实例,再把这个实例用子类再实例一遍
instanceof关键字
Java中可以使用instanceof关键字判断一个对象是否是某个类(或接口)的实例 语法 : 对象 instanceof类(或接口) 在上述格式中,如果对象是指定的类的实例对象,则返回true,否则返回false
2.11 Object类
Java提供了一个Object类,它是所有类的父类,每个类都直接或间接继承Object类,因此Object类通常被称之为超类。当定义一个类时,如果没有使用extends关键字为这个类显式地指定父类,那么该类会默认继承Object类
方法名称 | 方法说明 |
---|
boolean equals() | 判断两个对象是否“相等” | int hashCode() | 返回对象的哈希码值 | String toString() | 返回对象的字符串表示形式 |
2.12 内部类
在Java中,允许在一个类的内部定义类,这样的类称作内部类,内部类所在的类称作外部类,成员内部类可以访问外部类的所有成员
1.成员内部类:在一个类中除了可以定义成员变量、成员方法,还可以定义类,这样的类被称作成员内部类。成员内部类可以访问外部类的所有成员
class Outer {
int m = 0;
void test1() {
System.out.println("外部类成员方法");
}
class Inner {
int n = 1;
void show1() {
System.out.println("外部成员变量m = " + m);
}
void show2() {
System.out.println("内部成员方法");
}
}
void test2() {
Inner inner = new Inner();
System.out.println("内部成员变量n = " + inner.n);
inner.show2();
}
}
public class Example20 {
public static void main(String[] args) {
Outer outer = new Outer();
Outer.Inner inner = new outer.new Inner();
inner.show1();
outer.test2();
}
}
如果想通过外部类访问内部类,则需要通过外部类创建内部类对象,创建内部类对象的具体语法格式如下: 外部类名.内部类名 变量名 = new 外部类名().new 内部类名();
(如上第28行代码)
2.局部内部类:也叫作方法内部类,是指定义在某个局部范围中的类,它和局部变量一样,都是在方法中定义的,有效范围只限于方法内部
class Outer{
int m = 0;
void test1(){
System.out.println("外部类成员方法");
}
void test2(){
class Inner{
int n = 1;
void show(){
System.out.println("外部成员变量m="+m);
test1();
}
}
Inner inner = new Inner();
System.out.println("局部内部成员变量n="+inner.n);
inner.show();
}
}
public class Examp21{
public static void main(String args[]){
Outer outer = new Outer();
outer.test2();
}
}
3.静态内部类
所谓静态内部类,就是使用static关键字修饰的成员内部类,与成员内部类相比,在形式上,静态内部类只是在内部类前增加了static关键字,但在功能上,静态内部类只能访问外部类的静态成员,通过外部类访问静态内部类成员时,可以跳过外部类直接访问静态内部类
创建静态内部类对象的基本语法格式如下: 外部类名.静态内部类名 变量名 = new 外部类名().静态内部类名();
静态内部类实例:
class Outer{
static int m = 0;
static class Inner{
int n = 1;
void show(){
System.out.println("外部静态变量m="+m);
}
}
}
public class Example{
public static void main(String args[]){
Outer.Inner inner = new Outer.Inner();
inner.show();
}
}
4.匿名内部类
匿名内部类是没有名称的内部类,在Java中调用某个方法时,如果该方法的参数是接口类型,除了可以传入一个接口实现类,还可以使用实现接口的匿名内部类作为参数,在匿名内部类中直接完成方法的实现
创建匿名内部类的基本语法格式如下: new 父接口(){ //匿名内部类实现部分 }
interface Animal{
void shout();
}
public class Example23{
public static void main(String[] args){
String name = "小花";
animalShout(new Animal(){
@Override
public void shout() {
System.out.println(name+"喵喵...");
}
});
}
public static void animalShout(Animal an){
an.shout();
}
}
匿名类的编写步骤: **(1)**在调用animalShout()方法时,在方法的参数位置写上new Animal(){},这相当于创建了一个实例对象,并将对象作为参数传给animalShout()方法,在new Animal()后面有一对大括号,表示创建的对象为Animal的子类实例,该子类是匿名的。具体代码如下所示: animalShout(new Animal(){});
**(2)**在大括号中编写匿名子类的实现代码,具体如下所示: animalShout(new Animal() { public void shout() { System.out.println(“喵喵……”); } });
2.13 异常
在程序运行的过程中,也会发生各种非正常状况,例如,程序运行时磁盘空间不足、网络连接中断、被装载的类不存在等。针对这种情况, Java语言引入了异常,以异常类的形式对这些非正常情况进行封装,通过异常处理机制对程序运行时发生的各种问题进行处理
算术异常案例:
public class Example24 {
public static void main(String[] args) {
int result = divide(4, 0);
System.out.println(result);
}
public static int divide(int x, int y) {
int result = x / y;
return result;
}
}
程序发生了算术异常(ArithmeticException),该异常是由于文件4-24中的第3行代码调用divide()方法时传入了参数0,运算时出现了被0除的情况。异常发生后,程序会立即结束,无法继续向下执行。
上述程序产生的ArithmeticException异常只是Java异常类中的一种,Java提供了大量的异常类,这些类都继承自java.lang.Throwable类 接下来通过一张图展示Throwable类的继承体系
Throwable有两个直接子类Error和Exception,其中,Error代表程序中产生的错误,Exception代表程序中产生的异常
● Error类称为错误类,它表示Java程序运行时产生的系统内部错误或资源耗尽的错误,这类错误比较严重,仅靠修改程序本身是不能恢复执行的。举一个生活中的例子,在盖楼的过程中因偷工减料,导致大楼坍塌,这就相当于一个Error。例如,使用java命令去运行一个不存在的类就会出现Error错误
● Exception类称为异常类,它表示程序本身可以处理的错误,在Java程序中进行的异常处理,都是针对Exception类及其子类的。在Exception类的众多子类中有一个特殊的子类—RuntimeException类,RuntimeException类及其子类用于表示运行时异常。 Exception类的其他子类都用于表示编译时异常
Throwable类中的常用方法如下表
方法声明 | 功能描述 |
---|
String getMessage() | 返回异常的消息字符串 | String toString() | 返回异常的简单信息描述 | void printStackTrace() | 获取异常类名和异常信息,以及异常出现在程序中的位置,把信息输出在控制台。 |
try…catch 和 finally
为了解决异常,Java提供了对异常进行处理的方式一一异常捕获,异常捕获使用try…catch语句实现,try…catch具体语法格式如下
try{
}catch(ExceptionType(Exception类及其子类) e){
}
finally关键字:在程序中,有时候会希望有些语句无论程序是否发生异常都要执行,这时就可以在try…catch语句后,加一个finally代码块
public class Example26 {
public static void main(String[] args) {
try {
int result = divide(4, 0);
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;
return result;
}
}
注意:finally中的代码块在一种情况下是不会执行的,那就是在try…catch中执行了System.exit(0)语句。System.exit(0)表示退出当前的Java虚拟机,Java虚拟机停止了,任何代码都不能再执行了
throws关键字
在实际开发中,大部分情况下我们会调用别人编写方法,并不知道别人编写的方法是否会发生异常。针对这种情况,Java允许在方法的后面使用throws关键字对外声明该方法有可能发生的异常,这样调用者在调用方法时,就明确地知道该方法有异常,并且必须在程序中对异常进行处理,否则编译无法通过
throws关键字声明抛出异常的语法格式如下: 修饰符 返回值类型 方法名(参数1,参数2…)throws 异常类1, 异常类2…{ //方法体… } 从上述语法格式中可以看出,throws关键字需要写在方法声明的后面,throws后面需要声明方法中发生异常的类型
throws关键字实例:
public class Example28 {
public static void main(String[] args) {
try {
int result = divide(4, 2);
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
}
public static int divide(int x, int y) throws Exception {
int result = x / y;
return result;
}
}
(1)由于使用了try…catch对divide()方法进行了异常处理,因此程序可以编译通过
(2)如果没有使用try…catch对divide()方法进行异常处理,在main()方法继续使用throws关键字将Exception抛出,程序虽然可以通过编译,但从运行结果可以看出,在运行时期由于没有对“/by zero”的异常进行处理,最终导致程序终止运行
运行时异常与编译时异常
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gbUmndsq-1645365280299)(光速回顾Java基础.assets/image-20211002151329142-16450503084191.png)]
在Exception类中,除了RuntimeException类及其子类,Exception的其他子类都是编译时异常,编译时异常的特点是Java编译器会对异常进行检查,如果出现异常就必须对异常进行处理,否则程序无法通过编译 处理编译时期的异常有两种方式,具体如下: (1)使用try…catch语句对异常进行捕获处理。 (2)使用throws关键字声明抛出异常,调用者对异常进行处理。
RuntimeException类及其子类都是运行时异常。运行时异常的特点是Java编译器不会对异常进行检查。也就是说,当程序中出现这类异常时,即使没有使用try…catch语句捕获或使用throws关键字声明抛出,程序也能编译通过,运行时异常一般是由程序中的逻辑错误引起的,在程序运行时无法恢复
自定义异常
JDK中定义了大量的异常类,虽然这些异常类可以描述编程时出现的大部分异常情况,但是在程序开发中有时可能需要描述程序中特有的异常情况
例如,前面讲解的程序中的divide()方法,不允许被除数为负数,为了解决这个问题,Java允许用户自定义异常,但自定义的异常类必须继承自Exception或其子类
自定义异常:
public class DivideMinusException extends Exception{
public DivideByMinusException(){
super();
}
public DivideByMinusException(String message){
super(massage);
}
}
在实际开发中,如果没有特殊的要求,自定义的异常类只需继承Exception类,在构造方法中使用super()语句调用Exception的构造方法即可。 自定义异常类中使用throw关键字在方法中声明异常的实例对象,格式如下: throw Exception异常对象
用throw关键字抛出异常对象时,需要使用try…catch语句对抛出的异常进行处理,或者在divide()方法上使用throws关键字声明抛出异常,由该方法的调用者负责处理
自定义异常实例:
public class Example{
public static void main(String args[]){
try{
int result = devide(4,-2);
System.out.println(result);
}
catch(DivideNyMinusException e){
System.out.println(e.getMessage);
}
}
public static int divide(int x,int y) throw DivideByMinusException{
if(y<0){
throw new DivideByMinusException("除非是负数");
}
int result = x/y;
return result;
}
}
第三重 凝神(Java常用API)
3.1 字符串类
Java中定义了三个封装字符串的类,分别是String、StringBuffer和StringBuilder,它们位于java.lang包中,并提供了一系列操作字符串的方法,这些方法不需要导包就可以直接使用
3.1.1 String类
String类的初始化:
1.使用字符串常量直接初始化一个String对象:String str1 = “abc”;
2.使用String类的构造方法初始化字符串对象
方法声明 | 功能描述 |
---|
String() | 创建一个内容为空的字符串 | String(String value) | 根据指定的字符串内容创建对象 | String(char[] value) | 根据指定的字符数组创建对象 | String(byte[] bytes) | 根据指定的字节数组创建对象 |
String类的常用操作:
1.字符串的获取功能:在Java程序中,需要对字符串进行一些获取的操作,如获得字符串长度、获得指定位置的字符等
实例:
1 public class Example02 {
2 public static void main(String[] args) {
3 String s = "ababcdedcba";
4
5 System.out.println("字符串的长度为:" + s.length());
6 System.out.println("字符串中第一个字符:" + s.charAt(0));
7 System.out.println("字符c第一次出现的位置:" + s.indexOf('c'));
8 System.out.println("字符c最后一次出现的位置:" + s.lastIndexOf('c'));
9 System.out.println("子字符串ab第一次出现的位置:" +
10 s.indexOf("ab"));
11 System.out.println("子字符串ab字符串最后一次出现的位置:" +
12 s.lastIndexOf("ab"));
13 }
14 }
-
str.length -> 获取字符串长度 -
str.chatAt(0) -> 获取字符串第一个字符(注意是从0开始数) -
str.indexxOf(‘a’) -> 获取字符 ’ a ’ 第一次出现的位置 -
str.lastIndexxOf(‘a’) -> 获取字符 ’ a ’ 最后一次出现的位置
2.字符串转换操作:程序开发中,经常需要对字符串进行转换操作。例如,将字符串转换成数组的形式,将字符串中的字符进行大小写转换等
实例:
1 public class Example03 {
2 public static void main(String[] args) {
3 String str = "abcd";
4 System.out.print("将字符串转为字符数组后的结果:");
5 char[] charArray = str.toCharArray();
6 for (int i = 0; i < charArray.length; i++) {
7 if (i != charArray.length - 1) {
8
9 System.out.print(charArray[i] + ",");
10 } else {
11
12 System.out.println(charArray[i]);
13
14 }
15 System.out.println("将int值转换为String类型之后的结果:" +
16 String.valueOf(12));
17 System.out.println("将字符串转换成大写之后的结果:" +
18 str.toUpperCase());
19 System.out.println("将字符串转换成小写之后的结果:" +
20 str.toLowerCase());
21 }
22 }
- str.toCharArray -> 字符串转换为数组
- str.toUpperCase - > 字符串内容转换为大写形式
- str.toLowerCase - > 字符串内容转换为小写形式
valueOf()方法有多种重载的形式,float、double、char等其他基本类型的数据都可以通过valueOf()方法转为String字符串类型
3.字符串的替换和去除空格操作:程序开发中,用户输入数据时经常会有一些错误和空格,这时可以使用String类的replace()和trim()方法,进行字符串的替换和去除空格操作
实例:
1 public class Example04 {
2 public static void main(String[] args) {
3 String s = "itcast";
4
5 System.out.println("将it替换成cn.it的结果:" + s.replace("it",
6 "cn.it"));
7
8 String s1 = " i t c a s t ";
9 System.out.println("去除字符串两端空格后的结果:" + s1.trim());
10 System.out.println("去除字符串中所有空格后的结果:" + s1.replace(" ",
11 ""));
12 }
13 }
- str.replace(“a”,"b) - > 注意顺序,是用后面的替换前面的
- str.trim -> 去除字符串两端空格
- str.replace(" “,”") -> 去除字符串中所有的空格(其实是通过replace语句将空格字符" “用空字符”"替代了)
4.字符串的判断操作:操作字符串时,经常需要对字符串进行一些判断,如判断字符串是否以指定的字符串开始、结束,是否包含指定的字符串,字符串是否为空等
实例:
1 public class Example05 {
2 public static void main(String[] args) {
3 String s1 = "String";
4 String s2 = "Str";
5 System.out.println("判断是否以字符串Str开头:" +
6 s1.startsWith("Str"));
7 System.out.println("判断是否以字符串ng结尾:" + s1.endsWith("ng"));
8 System.out.println("判断是否包含字符串tri:" + s1.contains("tri"));
9 System.out.println("判断字符串是否为空:" + s1.isEmpty());
10 System.out.println("判断两个字符串是否相等" + s1.equals(s2));
11 }
12 }
- str.starWith(“a”) -> 判断字符串是否以"a"开头
- str.endWith(“b”) -> 判断字符串是否以"b"结束
- str.contains(“c”) -> 判断字符串是否包含"c"
- str.isEmpty() -> 判断字符串是否为空
- str1.equals(str2) -> 判断两个字符串是否相等
在程序中可以通过“==”和equals()两种方式对字符串进行比较,但这两种方式有明显的区别
(1)equals()方法用于比较两个字符串中的字符是否相等
(2)==方法用于比较两个字符串对象的地址是否相同
也就是说,对于两个内容完全一样的字符串对象,使用equals判断的结果是true,使用==判断的结果是false。
5.字符串的截取和分割:在String类中,substring()方法用于截取字符串的一部分,split()方法用于将字符串按照某个字符进行分割
实例:
1 public class Example06 {
2 public static void main(String[] args) {
3 String str = "石家庄-武汉-哈尔滨";
4
5 System.out.println("从第5个字符截取到末尾的结果:" +
6 str.substring(4));
7 System.out.println("从第5个字符截取到第6个字符的结果:" +
8 str.substring(4, 6));
9
10 System.out.print("分割后的字符串数组中的元素依次为:");
11 String[] strArray = str.split("-");
12 for (int i = 0; i < strArray.length; i++) {
13 if (i != strArray.length - 1) {
14
15 System.out.print(strArray[i] + ",");
16 } else {
17
18 System.out.println(strArray[i]);
19 }
20 }
21 }
22 }
-
str.substring(3,5) -> 从第四个字符截取到第五个字符(注意第一个参数为下标,第二个参数为位置) -
str.substring(3) -> 从第四个字符开始一直截取到最后一个字符 -
str.split("-") -> 以"-"为分隔符,将原字符串分割为一段一段的小字符串 (常与 String[] strArry = str.split("-")联用,意在将分割出来的小段字符串依次存入数组)
String字符串在获取某个字符时,会用到字符的索引,当访问字符串中的字符时,如果字符的索引不存在,则会发生StringIndexOutOfBoundsException(字符串角标越界异常)
3.1.2 StringBuffer类
由于字符串是常量,因此一旦创建,其内容和长度是不可改变的。如果需要对一个字符串进行修改,则只能创建新的字符串。为了对字符串进行修改,Java提供了一个StringBuffer类**(也称字符串缓冲区)**
StringBuffer与String的最大区别在于其内容和长度都是可变的,其类似于一个字符容器,对其内容进行修改时不会产生新的StringBuffer对象
实例:
1public class Example08 {
2 public static void main(String[] args) {
3 System.out.println("1、添加------------------------");
4 add();
5 System.out.println("2、删除------------------------");
6 remove();
7 System.out.println("3、修改------------------------");
8 alter();
9 }
10 public static void add() {
11 StringBuffer sb = new StringBuffer();
12 sb.append("abcdefg");
13 System.out.println("append添加结果:" + sb);
14 sb.insert(2, "123");
15 System.out.println("insert添加结果:" + sb);
16 }
17 public static void remove() {
18 StringBuffer sb = new StringBuffer("abcdefg");
19 sb.delete(1, 5);
20 System.out.println("删除指定位置结果:" + sb);
21 sb.deleteCharAt(2);
22 System.out.println("删除指定位置结果:" + sb);
23 sb.delete(0, sb.length());
24 System.out.println("清空缓冲区结果:" + sb);
25 }
26 public static void alter() {
27 StringBuffer sb = new StringBuffer("abcdef");
28 sb.setCharAt(1, 'p');
29 System.out.println("修改指定位置字符结果:" + sb);
30 sb.replace(1, 3, "qq");
31 System.out.println("替换指定位置字符(串)结果:" + sb);
32 System.out.println("字符串翻转结果:" + sb.reverse());
33 }
34 }
- 添加操作
- sb.append(“abc”) -> 在字符串尾部追加"abc"
- sb.insert(3,“a”) -> 在下标为3的位置添加"a"
- 删除操作
- sb.delete(1,5) -> 指定位置删除(与String类的1substring方法相似,第一个参数为下标,第二个参数为位置)
- sb.delete(2) -> 指定位置删除,删除下标为2的字符
- sb.delete(0,sb.length) -> 清空缓冲区的操作
- 修改操作
- sb.setChar(1,“a”) -> 指定位置修改,将下标为1的字符修改为"a"
- sb.replace(1,3,“qq”) -> 指定位置修改,将下标1到位置3的字符段修改为"qq"
3.1.3 StringBuilder类
StringBuilder类与StringBuffer类相似,StringBuilder类也可以对字符串进行修改,StringBuffer类和StringBuilder类的对象都可以被多次修改,并不产生新的未使用对象
StringBuilder类是JDK5中新加的类,它与StringBuffer之间最大不同在于StringBuffer的方法是线程安全的,也就是说StringBuilder不能被同步访问,而StringBuffer可以
但是相较于StringBuffer而言StringBuilder有速度优势
3.1.5 String、StringBuffer和StringBuilder三种字符串类的比较
(1)String类表示的字符串是常量,一旦创建后,内容和长度都是无法改变的。而StringBuilder和StringBuffer表示字符容器,其内容和长度可以随时修改
? 字符串仅用于表示数据类型 -> 使用String类
? 需要对字符串中的字符进行增删操作 -> 使用StringBuffer与StringBuilder类
? 有大量字符串拼接操作,不要求线程安全的情况 -> 采用StringBuilder更高效
? 有大量字符串拼接操作,如果需要线程安全 -> 使用StringBuffer
(StringBuffer多线程安全性大于StringBuilder)
(2)对于euals()方法的使用我们已经有所了解,但是在StringBuffer类与StringBuilder类中并没有被Object类的equals()方法覆盖,也就是说,equals()方法对于StringBuffer类与StringBuilder类来言并不起作用
(3)String类对象可以用操作符“+”进行连接,而StringBuffer类对象之间不能
3.2 System类与RunTime类
3.2.1 System类
System类定义了一些与系统相关的属性和方法,它所提供的属性和方法都是静态的,因此,想要引用这些属性和方法,直接使用System类调用即可
System类的常用方法:
- arraycopy()方法:
arraycopy()方法用于将数组从源数组复制到目标数组,声明格式如下:
static void arraycopy(Object src,int srcPos,Object dest, int destPos,int length)
● src:表示源数组。 ● dest:表示目标数组。 ● srcPos:表示源数组中拷贝元素的起始位置。 ● destPos:表示拷贝到目标数组的起始位置。 ● length:表示拷贝元素的个数。
在进行数组复制时,目标数组必须有足够的空间来存放拷贝的元素,否则会发生角标越界异常
实例:
public class Example10 {
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.println(i + ": " + toArray[i]);
}
}
}
? 2.currentTimeMillis()方法:
currentTimeMillis()方法用于获取当前系统的时间,返回值是long类型的值,该值表示当前时间与1970年1月1日0点0分0秒之间的时间差,单位是毫秒,通常也将该值称作时间戳
实例:
public class Example11 {
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对象,其中封装了系统的所有属性,这些属性是以键值对形式存在的。getProperty() 方法用于根据系统的属性名获取对应的属性值
(系统的Properties类)
? 4.gc()方法
在Java中,当一个对象成为垃圾后仍会占用内存空间,时间一长,就会导致内存空间的不足。针对这种情况,Java中引入了垃圾回收机制,有了这种机制,程序员不需要过多关心垃圾对象回收的问题,Java虚拟机会自动回收垃圾对象所占用的内存空间
除了等待Java虚拟机进行自动垃圾回收外,还可以通过调用System.gc()方法通知Java虚拟机立即进行垃圾回收,当一个对象在内存中被释放时,它的finalize()方法会被自动调用,因此可以在类中通过定义finalize()方法观察对象何时被释放
实例
class Person {
public void finalize() {
System.out.println("对象将被作为垃圾回收...");
}
}
public class Example13{
public static void main(String[] args) {
Person p1 = new Person();
Person p2 = new Person();
p1 = null;
p2 = null;
System.gc();
for (int i = 0; i < 1000000; i++) {
}
}
}
Java虚拟机的垃圾回收操作是在后台完成的,程序结束后,垃圾回收的操作也将终止
System类还有一个常见的方法exit(int status),该方法用于终止当前正在运行的Java虚拟机,其中参数status用于表示当前发生的异常状态,通常指定为0,表示正常退出,否则表示异常终止
3.2.2 RunTime类
表示虚拟机运行时的状态,它用于封装JVM虚拟机进程。每次使用java命令启动虚拟机都对应一个Runtime实例,并且只有一个实例,因此在Runtime类定义的时候,它的构造方法已经被私有化了(单例设计模式的应用),对象不可以直接实例化。若想在程序中获得一个Runtime实例,只能通过以下方式:
Runtime run = Runtime.getRuntime();
Runtime类的常用方法
方法声明 | 功能描述 |
---|
getRuntime() | 该方法用于返回当前应用程序的运行环境对象。 | exec(String command) | 该方法用于根据指定的路径执行对应的可执行文件 | freeMemory**()** | 该方法用于返回Java虚拟机中的空闲内存量,以字节为单位。 | maxMemory() | 该方法用于返回Java虚拟机的最大可用内存量。 | availableProcessors() | 该方法用于返回当前虚拟机的处理器个数 | totalMemory() | 该方法用于返回Java虚拟机中的内存总量 |
Runtime类的使用:
? 1.获取当前虚拟机信息
Runtime类可以获取当前Java虚拟机的处理器的个数、空闲内存量、最大可用内存量和内存总量的信息
1 public class Example14 {
2 public static void main(String[] args) {
3 Runtime rt = Runtime.getRuntime();
4 System.out.println("处理器的个数: " + rt.availableProcessors()+"个
5 ");
6 System.out.println("空闲内存数量: " + rt.freeMemory() / 1024 / 1024
7 + "M");
8 System.out.println("最大可用内存数量: " + rt.maxMemory() / 1024 /
9 1024 + "M");
10 System.out.println("虚拟机中内存总量: " + rt.totalMemory() / 1024 /
11 1024 + "M");
12 }
13 }
? 2.操作系统进程
Runtime类中提供了一个exec()方法,该方法用于执行一个dos命令,从而实现和在命令行窗口中输入dos命令同样的效果。例如,通过运行“notepad.exe”命令打开一个Windows自带的记事本程序
1 import java.io.IOException;
2 public class Example15{
3 public static void main(String[] args) throws IOException {
4 Runtime rt = Runtime.getRuntime();
5 rt.exec("notepad.exe");
6 }
7 }
查阅API文档会发现,Runtime类的exec()方法返回一个Process对象,该对象就是exec()所生成的新进程,通过该对象可以对产生的新进程进行管理,如关闭此进程只需调用destroy()方法即可
public class Example {
public static void main(String[] args) throws Exception {
Runtime rt = Runtime.getRuntime();
Process process = rt.exec("notepad.exe");
Thread.sleep(3000);
process.destroy();
}
}
3.3 Math类与Random类
3.3.1 Math类
方法声明 | 功能描述 |
---|
abs() | 该方法用于计算绝对值 | sqrt() | 该方法用于计算方根 | ceil(a,b) | 该方法用于计算大于参数的最小整数 | floor() | 该方法用于计算小于参数的最小整数 | round() | 该方法用于计算小数进行四舍五入后的结果 | max() | 该方法用于计算两个数的较大值 | min() | 该方法用于计算两个数的较小值 | random() | 该方法用于生成一个大于0.0小于1.0的随机值 | sqrt() | 该方法用于计算开平方的结果 | pow() | 该方法用于计算指数函数的值 |
实例
public class Example16 {
public static void main(String[] args) {
System.out.println("计算绝对值的结果: " + Math.abs(-10));
System.out.println("求大于参数的最小整数: " + Math.ceil(5.6));
System.out.println("求小于参数的最大整数: " + Math.floor(-4.2));
System.out.println("对小数进行四舍五入后的结果: " + Math.round(-4.6));
System.out.println("求两个数的较大值: " + Math.max(2.1, -2.1));
System.out.println("求两个数的较小值: " + Math.min(2.1, -2.1));
System.out.println("生成一个大于等于0.0小于1.0随机值: " +
Math.random());
System.out.println("开平方的结果: "+Math.sqrt(4));
System.out.println("指数函数的值: "+Math.pow(2, 3));
}
}
3.3.2 Random类
Java的java.util包中有一个Random类,它可以在指定的取值范围内随机产生数字
方法声明 | 功能描述 |
---|
Random() | 构造方法,用于创建一个伪随机数生成器 | Random(long seed) | 构造方法,使用一个long型的seed种子创建伪随机数生成器 |
Random类的两个构造方法:
其中第一个构造方法是无参的,通过它创建的Random实例对象每次使用的种子是随机的,因此每个对象所产生的随机数不同
如果希望创建的多个Random实例对象产生相同的随机数,则可以在创建对象时调用第二个构造方法,传入相同的参数即可
第一种构造方法:
import java.util.Random;
public class Example17 {
public static void main(String args[]) {
Random r = new Random();
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(13);
for (int x = 0; x < 10; x++) {
System.out.println(r.nextInt(100));
}
}
}
Random类的nextDouble()方法返回的是0.0和1.0之间double类型的值
nextFloat()方法返回的是0.0和1.0之间float类型的值
nextInt(int n)返回的是0(包括)和指定值n(不包括)之间的值(左闭右开区间)
import java.util.Random;
public class Example19 {
public static void main(String[] args) {
Random r1 = new Random();
System.out.println("产生float类型随机数: " + r1.nextFloat());
System.out.println("产生double类型的随机数:" + r1.nextDouble());
System.out.println("产生int类型的随机数:" + r1.nextInt());
System.out.println("产生0~100之间int类型的随机数:" +
r1.nextInt(100));
}
}
3.4 日期时间类
在开发中经常需要处理日期和时间,Java提供了一套专门用于处理日期时间的API,在日期时间类中了包含LocalDate类、LocalTime类、Instant类、Duration类以及Period类等,这些类都包含在java.time包中
类的名称 | 功能描述 |
---|
Instant | 表示时刻,代表的是时间戳 | LocalDate | 不包含具体时间的日期 | LocalTime | 不含日期的时间 | LocalDateTime | 包含了日期及时间 | Duration | 基于时间的值测量时间量 | Period | 计算日期时间差异,只能精确到年月日 | Clock | 时钟系统,用于查找当前时刻 |
3.4.1 Instant类
Instant 类代表的是某个时刻(时间戳),其内部是由两个Long字段组成,第一部分保存的是标准Java计算时代(就是1970年1月1日开始)到现在的秒数,第二部分保存的是纳秒数
3.4.2 LocalData类
LocalData类仅用来表示日期。通常表示的是年份和月份,该类不能代表时间线上的即时信息,只是日期的描述
在LocalData类中提供了两个获取日期对象的方法now()和of(int year, int month, int dayOfMonth)
LocalData还提供了日期格式化、增减年月日等一系列的常用方法
方法声明 | 功能描述 |
---|
getYear() | 获取年份字段 | getMonth() | 使用Month枚举获取月份字段 | getMonthValue() | 将月份字段从1到12 | getDayOfMonth() | 获取当月第几天字段 | format(DateTimeFormatter formatter) | 使用指定的格式化程序格式化此日期。 | isBefore(ChronoLocalDate other) | 检查此日期是否在指定日期之前。 | isAfter(ChronoLocalDate other) | 检查此日期是否在指定日期之后。 | isEqual(ChronoLocalDate other) | 检查此日期是否等于指定的日期。 | isLeapYear() | 根据ISO培训日历系统规则,检查年份是否是闰年。 |
实例:
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
public class Example21 {
public static void main(String[] args) {
LocalDate now = LocalDate.now();
LocalDate of = LocalDate.of(2015, 12, 12);
System.out.println("1. LocalData的获取及格式化的相关方法--------");
System.out.println("从LocalData实例获取的年份为:"+now.getYear());
System.out.println("从LocalData实例获取的月份:"
+now.getMonthValue());
System.out.println("从LocalData实例获取当天在本月的第几天:"+
now.getDayOfMonth());
System.out.println("将获取到的Loacaldata实例格式化为:"+
now.format(DateTimeFormatter.ofPattern("yyyy年MM月dd日")));
System.out.println("2. LocalData判断的相关方法----------------");
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. LocalData解析以及加减操作的相关方法---------");
String dateStr="2020-02-01";
System.out.println("把日期字符串解析成日期对象后为"+
LocalDate.parse(dateStr));
System.out.println("将LocalData实例年份加1为:"+now.plusYears(1));
System.out.println("将LocalData实例天数减10为:"
+now.minusDays(10));
System.out.println("将LocalData实例指定年份为2014:"+
now.withYear(2014));
}
}
3.4.3 LocalTime类 与 LocalDataTime类
LocalTime类用来表示时间,通常表示的是小时分钟秒。与LocalData类一样,该类不能代表时间线上的即时信息,只是时间的描述。在LocalTime类中提供了获取时间对象的方法,与LocalData用法类似。 同时LocalTime类也提供了与日期类相对应的时间格式化、增减时分秒等常用方法,这些方法与日期类相对应
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
public class Example22 {
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));
}
}
3.4.4 Period类 和 Duration类
? 1.Duration类
Duration类基于时间值,其作用范围是天、时、分、秒、毫秒和纳秒
方法声明 | 功能描述 |
---|
between(Temporal startInclusive, Temporal endExclusive) | 获取一个Duration表示两个时间对象之间的持续时间。 | toDays(): | 将时间转换为以天为单位的 | toHours(): | 将时间转换为以时为单位的 | toMinutes(): | 将时间转换为以分钟为单位的 | toMillis(): | 将时间转换为以毫秒为单位的 | toNanos(): | 将时间转换为以纳秒为单位的 |
实例
import java.time.Duration;
import java.time.LocalTime;
public class Example24{
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()+"小时");
}
}
? 2.Period类
Period主要用于计算两个日期的间隔,与Duration相同,也是通过between计算日期间隔,并提供了获取年月日的三个常用方法,分别是 getYears()、getMonths()和getDays()
实例:
import java.time.LocalDate;
import java.time.Period;
public class Example25 {
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()+"天");
}
}
3.5 包装类
Java是一种面向对象的语言,Java中的类可以把方法与数据连接在一起,但是Java语言中却不能把基本的数据类型作为对象来处理
而某些场合下可能需要把基本数据类型的数据作为对象来使用,为了解决这样的问题,JDK中提供了一系列的包装类,可以把基本数据类型的值包装为引用数据类型的对象,在Java中,每种基本类型都有对应的包装类
基本数据类型 | 对应的包装类 |
---|
byte | Byte | char | Character | int | Integer | short | Short | long | Long | float | Float | double | Double | boolean | Boolean |
包装类和基本数据类型在进行转换时,引入了装箱和拆箱的概念,其中装箱是指将基本数据类型的值转为引用数据类型,反之,拆箱是指将引用数据类型的对象转为基本数据类型
装箱和拆箱的实例:
public class Example{
public static void main(String[] args){
int a = 20;
Integer in = a;
System.out.println(in);
int l = in;
System.out.println(l);
}
}
首先要有一个声明好的指定数据类型的变量a(假设),然后进行装箱操作,即用该数据类型的包装类创建对象,在创建时将该指定数据类型的变量a作为参数传入,从而转为包装类类型,在执行完相关操作后,在将该包装类拆箱,在创建基本数据类型时,再将该包装类的值直接赋给新的基本数据类型的变量
Integer类除了具有Object类的所有方法外,还有一些特有的方法
方法声明 | 功能描述 |
---|
Integer valueOf(int i) | 返回一个表示指定的int值的 Integer 实例 | Integer valueOf(String s) | 返回保存指定的String的值的 Integer 对象 | int parseInt(String s) | 将字符串参数作为有符号的十进制整数进行解析 | intValue() | 将 Integer 类型的值以int类型返回 |
-
intValue()方法可以将Integer类型的值转为int类型,这个方法可以用来进行手动拆箱操作 -
parseInt(String s)方法可以将一个字符串形式的数值转成int类型 -
valueOf(int i)可以返回指定的int值为Integer实例进行手动装箱
包装类实例
public class Example27 {
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);
}
}
使用包装类时的注意点:
-
包装类都重写了Object类中的toString()方法,以字符串的形式返回被包装的基本数据类型的值 -
除了Character外,包装类都有valueOf(String s)方法,可以根据String类型的参数创建包装类对象,但参数字符串s不能为null,而且字符串必须是可以解析为相应基本类型的数据,否则虽然编译通过,但运行时会报错 -
除了Character外,包装类都有parseXxx(String s)的静态方法,将字符串转换为对应的基本类型的数据。参数s不能为null,而且同样字符串必须可以解析为相应基本类型的数据,否则虽然编译通过,但运行时会报错
5.6 Pattern类和Matcher类
5.6.1 Pattern类
Pattern类用于创建一个正则表达式,也可以说创建一个匹配模式
它的构造方法是私有的,不可以直接创建,但可以通过Pattern.complie(String regex)简单工厂方法创建一个正则表达式,具体代码如下所示:
Pattern p=Pattern.compile("\w+");
方法声明 | 功能描述 |
---|
split(CharSequence input) | 将给定的输入序列分成这个模式的匹配 | Matcher matcher(CharSequence input) | 创建一个匹配器,匹配给定的输入与此模式 | Static boolean matches(String regex, CharSequence input) | 编译给定的正则表达式,并尝试匹配给定的输入 |
5.6.2 Matcher类
方法声明 | 功能描述 |
---|
boolean matches() | 对整个字符串进行匹配,只有整个字符串都匹配了才返回true | boolean lookingAt() | 对前面的字符串进行匹配,只有匹配到的字符串在最前面才返回true | boolean find() | 对字符串进行匹配,匹配到的字符串可以在任何位置 | int end() | 返回最后一个字符匹配后的偏移量 | string group() | 返回匹配到的子字符串 | int start() | 返回匹配到的子字符串在字符串中的索引位置 |
实例
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Example29 {
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("字符串是否匹配:"+ m2.matches());
System.out.println("对前面的字符串匹配结果为"+ m.lookingAt());
Matcher m3=p.matcher("aa2223");
System.out.println("对前面的字符串匹配结果为:"+m3.lookingAt());
m.find();
System.out.println("字符串任何位置是否匹配:"+ m.find());
m3.find();
System.out.println("字符串任何位置是否匹配:"+ m3.find());
Matcher m4=p.matcher("aabb");
System.out.println("字符串任何位置是否匹配:"+ m4.find());
Matcher m1=p.matcher("aaa2223bb");
m1.find();
System.out.println("上一个匹配的起始索引::"+ m1.start());
System.out.println("最后一个字符匹配后的偏移量"+ m1.end());
System.out.println("匹配到的子字符串:"+ m1.group());
}
}
5.6.3 String类对正则表达式的支持
String类提供了3个方法支持正则操作
方法声明 | 功能描述 |
---|
boolean matches(String regex) | 匹配字符串 | String replaceAll(String regex, String replacement) | 字符串替换 | String[] split(String regex) | 字符串拆分 |
public class Example30{
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]+" ");
}
}
}
第四重 结丹(Java集合类)
为了在程序中可以保存数目不确定的对象,Java提供了一系列特殊的类,这些类可以存储任意类型的对象,并且长度可变,这些类被统称为集合,集合类都位于java.util包中,使用时必须导包
集合按照其存储结构可以分为两大类,单列集合Collection和双列集合Map,这两种集合的特点具体如下:
集合按照其存储结构可以分为两大类,单列集合Collection和双列集合Map,这两种集合的特点具体如下:
● Collection:单列集合类的根接口,用于存储一系列符合某种规则的元素,它有两个重要的子接口,分别是List和Set。其中,List的特点是元素有序、元素可重复。Set的特点是元素无序,而且不可重复。List接口的主要实现类有ArrayList和LinkedList,Set接口的主要实现类有HashSet和TreeSe
● Map:双列集合类的根接口,用于存储具有键(Key)、值(Value)映射关系的元素,每个元素都包含一对键值,其中键值不可重复并且每个键最多只能映射到一个值,在使用Map集合时可以通过指定的Key找到对应的Value。例如,根据一个学生的学号就可以找到对应的学生。Map接口的主要实现类有HashMap和TreeMap
集合类的继承体系:
4.1 Collection接口
Collection是所有单列集合的父接口,它定义了单列集合(List和Set)通用的一些方法,这些方法可用于操作所有的单列集合。Collection接口的常用如下表
方法声明 | 功能描述 |
---|
boolean add(Object o) | 向集合中添加一个元素 | boolean addAll(Collection c) | 将指定Collection中的所有元素添加到该集合中 | void clear() | 删除该集合中的所有元素 | boolean remove(Object o) | 删除该集合中指定的元素 | boolean removeAll(Collection c) | 删除指定集合中的所有元素 | boolean isEmpty**()** | 判断该集合是否为空 | boolean contains(Object o) | 判断该集合中是否包含某个元素 | boolean containsAll**(Collection c)** | 判断该集合中是否包含指定集合中的所有元素 | Iterator iterator() | 返回在该集合的元素上进行迭代的迭代器(Iterator),用于遍历该集合所有元素 | int size() | 获取该集合元素个数 |
List接口简介:
List接口继承自Collection接口,其不但继承了Collection接口中的方法增加了一些根据元素索引操作集合的特有方法,是单列集合的一个重要分支
List接口的特点:
- 允许出现重复的元素,所有的元素是以一种线性的方式存储的,在程序中可以通过索引访问List集合中的指定元素
- List集合元素有序,即元素的存入顺序和取出顺序一致
4.1.1 ArrayList集合(查找)
ArrayList:ArrayList是List接口的一个实现类,它是程序中最常见的一种集合,在ArrayList内部封装了一个长度可变的数组对象,当存入的元素超过数组长度时,ArrayList会在内存中分配一个更大的数组来存储这些元素,因此可以将ArrayList集合看作一个长度可变的数组
语法:ArrayList Arr1 = new ArrayList<>();
ArrayList实现类的构造方法:
1.ArrayList(); //构造一个向量空间,使其内部数据数组的大小为10,其标准容量增量为0
2.ArrayList(int a); //使用指定的初始容量和容量增量构造一个向量空间
ArrayList实现类的一些常用方法:
增加元素:
add(E element); //将指定元素增加到末尾
add(int index,E element); //在指定位置插入指定元素
删除元素:
remove(int index); //移除指定位置的元素
clear(); //清空向量中所有的元素
修改元素:
set(int index,E element); //用指定的元素替代指定位置的元素
查找元素:
get(int index); //返回指定位置的元素
indexOf(Object o); //返回此向量中第一次出现指定元素的索引,若不存在指定元素则返回-1
lastIndexOf(Object o); //返回此向量中最后一次出现指定元素的索引,不存在返回-1
容器大小:
size(); //返回该容器中的组件数
判空:
isEmpty(); //判断该向量中是否包含组件
转化为数组:
toArray(); //返回一个数组,包含此向量中以恰当顺序存放的所有元素
转化为字符串:
toString(); //返回此向量的字符串表示形式,其中包括每个元素的String形式
4.1.2 LinkedList集合(增删)
ArrayList集合在查询元素时速度很快,但在增删元素时效率较低,为了克服这种局限性,可以使用List接口的另一个实现类LinkedList
LinkedList集合内部维护了一个双向循环链表,链表中的每一个元素都使用引用的方式来记住它的前一个元素和后一个元素,从而可以将所有的元素彼此连接起来。当插入一个新元素时,只需要修改元素之间的这种引用关系即可,删除一个节点也是如此。正因为这样的存储结构,所以LinkedList集合对于元素的增删操作具有很高的效率
LinkedList集合添加删除元素的过程:
方法声明 | 功能描述 |
---|
void add(int index, E element) | 在此列表中指定的位置插入指定的元素 | void addFirst(Object o) | 将指定元素插入此列表的开头 | void addLast(Object o) | 将指定元素添加到此列表的结尾 | Object getFirst() | 返回此列表的第一个元素 | Object getLast() | 返回此列表的最后一个元素 | Object removeFirst() | 移除并返回此列表的第一个元素 | Object removeLast() | 移除并返回此列表的最后一个元素 |
实例:
import java.util.*;
public class Example02 {
public static void main(String[] args) {
LinkedList link = new LinkedList();
link.add("张三");
link.add("李四");
link.add("王五");
link.add("赵六");
System.out.println(link.toString());
link.add(3, "Student");
link.addFirst("First");
System.out.println(link);
System.out.println(link.getFirst());
link.remove(3);
link.removeFirst();
System.out.println(link);
}
}
4.1.3 Iterator接口
Iterator接口也是集合中的一员,但它与Collection、Map接口有所不同
Collection接口与Map接口主要用于存储元素,而Iterator主要用于迭代访问(即遍历)Collection中的元素,因此Iterator对象也被称为迭代器
实例:
import java.util.*;
public class Example03 {
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add("张三");
list.add("李四");
list.add("王五");
list.add("赵六");
Iterator it = list.iterator();
while (it.hasNext()) {
Object obj = it.next();
System.out.println(obj);
}
}
}
核心代码:
Iterator it = list.iterator();
while(it.hasNext()){
Object obj = it.next();
System.out.println(obj);
}
迭代原理:内部采用指针的方式来跟踪集合中的元素
通过迭代器获取ArrayList集合中的元素时,这些元素的类型都是Object类型,如果想获取到特定类型的元素,则需要进行对数据类型强制转换
Iterator迭代器适合用于元素的遍历,不便于元素的修改:
import java.util.*;
public class Example04 {
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add("张三");
list.add("李四");
list.add("王五");
Iterator it = list.iterator();
while (it.hasNext()) {
Object obj = it.next();
if ("张三".equals(obj)) {
list.remove(obj);
}
}
System.out.println(list);
}
}
上述程序在运行时出现了并发修改异常ConcurrentModificationException
这个异常是迭代器对象抛出的,出现异常的原因是集合在迭代器运行期间删除了元素,会导致迭代器预期的迭代次数发生改变,导致迭代器的结果不准确
if ("张三".equals(obj)) {
list.remove(obj);
break;
}
if ("张三".equals(obj)) {
it.remove();
}
4.1.4 foreach循环
Iterator可以用来遍历集合中的元素,但写法上比较繁琐,为了简化书写,从JDK5开始,提供了foreach循环
foreach循环是一种更加简洁的for循环,也称增强for循环,foreach循环用于遍历数组或集合中的元素,具体语法格式如下:
for(容器中元素类型 临时变量 :容器变量) {
执行语句
}
与for循环相比,foreach循环不需要获得容器的长度,也不需要根据索引访问容器中的元素,但它会自动遍历容器中的每个元素
实例:
import java.util.*;
public class Example05 {
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add("aaa");
list.add("bbb");
list.add("ccc");
for (Object obj : list) {
System.out.println(obj);
}
}
}
**缺陷:**foreach循环虽然书写起来很简洁,但在使用时也存在一定的局限性。当使用foreach循环遍历集合和数组时,只能访问集合中的元素,不能对其中的元素进行修改
4.1.5 HashSet接口
HashSet是Set接口的一个实现类,Set接口实现类的特点是元素无序且存储的元素不可重复
实例:
import java.util.*;
public class Example07 {
public static void main(String[] args) {
HashSet set = new HashSet();
set.add("张三");
set.add("李四");
set.add("王五");
set.add("李四");
Iterator it = set.iterator();
while (it.hasNext()) {
Object obj = it.next();
System.out.println(obj);
}
}
}
打印输出结果中重复的“李四”被去除,且输出顺序与输入顺序不一致
**HashSet集合确保元素不重复的机理:**当调用HashSet集合的add()方法存入元素时,首先调用当前存入对象的hashCode()方法获得对象的哈希值,然后根据对象的哈希值计算出一个存储位置,如果这个存储位置上没有元素则直接存储,若有元素会调用equal()方法让当前存入的元素依次和该位置上的元素进行比较,如果返回值为false则将该元素存入,如果返回值为true说明有重复元素,就将该元素舍弃
实例:
import java.util.*;
class Student {
String id;
String name;
public Student(String id,String name) {
this.id=id;
this.name = name;
}
public String toString() {
return id+":"+name;
}
}
public class Example08 {
public static void main(String[] args) {
HashSet hs = new HashSet();
Student stu1 = new Student("1", "张三");
Student stu2 = new Student("2", "李四");
Student stu3 = new Student("2", "李四");
hs.add(stu1);
hs.add(stu2);
hs.add(stu3);
System.out.println(hs);
}
}
代码解析:
-
要点1:HashSet集合里的元素可以是实例化的对象(元素的数据类型可以是类) -
要点2:使用System.out.println(hs); 将HashSet类的对象hs输出时,系统会自动调用其元素的toString()方法,注意也就是Student类的toString()方法,这个时候我们为了改变输出形式会重写Student类默认的toString()方法 -
要点3:上述代码的输出结果中存在重复的“2,李四”,因为我们在定义Student类时没有重写hashCode()方法和equal()方法
改进:
import java.util.*;
class Student {
private String id;
private String name;
public Student(String id, String name) {
this.id = id;
this.name = name;
}
public String toString() {
return id + ":" + name;
}
public int hashCode() {
return id.hashCode();
}
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Student)) {
return false;
}
Student stu = (Student) obj;
boolean b = this.id.equals(stu.id);
return b;
}
}
public class Example09 {
public static void main(String[] args) {
HashSet hs = new HashSet();
Student stu1 = new Student("1", "张三");
Student stu2 = new Student("2", "李四");
Student stu3 = new Student("2", "李四");
hs.add(stu1);
hs.add(stu2);
hs.add(stu3);
System.out.println(hs);
}
}
-
要点1:通过重写Student类的hashCode()方法和equal()方法可以解决出现重复集合元素的问题 -
要点2:重写hashCode()方法,因为HashSet集合在存储元素时首先要计算这个对象的哈希值,再通过哈希值计算出一个存储位置,而Student类有两个成员变量,hashCode()方法无法正常使用,需要将hashCode()方法修改为返回其中一个成员变量的哈希值,HasSet集合的存储操作才能正常通过对象的哈希值计算出存储位置 -
要点3:重写equal()方法,在重写equal()方法的过程中,因为在通过哈希值计算出存储位置后,要通过equal()让当前存入的元素依次与该位置的元素进行比较,在比对到第n个对象时,这个对象是类的实例化,其继承了类的equal()方法,所以即将存入的对象要和第n个对象比对,即调用第n个对象的equal()方法
1.如果第19行代码确定是同一个元素,就直接返回true,即确定有重复元素,将其舍弃即可,在确定完不是同一个对象后
2.在第22行代码判断这个要存入HashSet集合的元素是否是Student类的实例化对象,如果不是同一个类的实例化对象,就可以直接返回false让HashSet集合的存储操作将其通过哈希值计算出的存储位置将其存储
3.在第25行代码开始,已经确定该对象是Student类且与第n个对象不是同一个对象,就通过Student stu = Student(obj)将其强转为Student类,然后与第n个元素的id值进行equal()方法进行比对,如果id相同,由于哈希值是通过id变量计算存储位置,若id相同,则计算出的哈希值就相同,存储位置也就相同,HashSet集合不允许这样的情况存在,所以返回true直接将这个元素舍弃,如果id值不同,说明通过哈希值计算出的存储位置不同,可以返回false直接将其存储
HashSet集合存储的元素是无序的,如果想让元素的存取顺序一致,可以使用Java中提供的LinkedHashSet类,LinkedHashSet类是HashSet的子类,与LinkedList一样,它也使用双向链表来维护内部元素的关系
4.1.6 TreeSet集合
HashSet集合存储的元素是无序的和不可重复的,为了对集合中的元素进行排序,Set接口提供了另一个可以对HashSet集合中元素进行排序的类——TreeSet
使用方法:
import java.util.TreeSet;
public class Example11 {
public static void main(String[] args) {
TreeSet ts = new TreeSet();
ts.add(3);
ts.add(1);
ts.add(1);
ts.add(2);
ts.add(3);
System.out.println(ts);
}
}
**输出结果:**通过TreeSet实现类的add方法添加元素后,在输出结果中已经对结果进行了排序,并且重复出现的元素只会打印一次
TreeSet集合对添加的元素进行排序的机理:元素的类可以实现Comparable接口(基本类型的包装类,String类都实现了该接口),Comparable接口强行对实现它的每个类的对象进行整体排序,这种排序方法称为自然排序
Comparable接口的compareTo()方法被称为自然比较方法,如果将自己定义的Student对象存入TreeSet集合,TreeSet将不会对添加的元素进行排序,Student类必须实现Comparable接口并重写compareTo()方法实现对元素的顺序存取(接口必须通过子类实现,并且子类必须实现接口的所有抽象方法)
将自定义对象存入TreeSet实现排序的两种方式:compareTo()的自然排序,Comparator接口的比较器排序
4.1.6.1 使用compareTo()方法实现对象元素的自然排序
import java.util.TreeSet;
class Student implements Comparable<Student>{
private String id;
private String name;
public Student(String id,String name){
this.id=id;
this.name=name;
}
public String toString(){
retutn id+":"+name;
}
@Override
public int compareTo(Student o){
return -1;
}
}
public class Example{
public static void main(String arsg[]){
TreeSet ts = new TreeSet();
ts.add(new Student("1","张三"));
ts.add(new Student("2","李四"));
ts.add(new Student("2","李四"));
System.out.println(ts);
}
}
class Student implements Comparable
尖括号<>内的Student代表Comparable接口包装的数据类型
4.1.6.2 通过实现Comparator接口进行比较器排序
**比较器排序:**实现Comparator接口,重写compare()方法和equals()方法,但是由于所有的类默认继承Object,而Object中有equals()方法,所有自定义比较器排序时不用重写equals()方法,只需要重写compare()方法,这种排序方法称为比较器排序法
import java.util.Comparator;
import java.util.TreeSet;
class Student{
private String id;
private String name;
public Student(String id,String name){
this.id=id;
this.name=name;
}
public String toString(){
retutn id+":"+name;
}
}
public class Example{
public static void main(String args[]){
TreeSet ts = new TreeSet(new Comparator(){
@Override
public int compare(Object o1,Object o2){
return -1;
}
});
ts.add(new Student("1","张三"));
ts.add(new Student("2","李四"));
ts.add(new Student("2","李四"));
System.out.println(ts);
}
第17~22行代码是声明了一个TreeSet集合并通过匿名内部类的方式实现了Comparator接口,然后重写了compare()方法
4.2 Map接口
Map接口是一种双列集合接口,它的每个元素都包含一个键对象Key和一个值对象Value,键和值对象之间存在一种关系,称为“映射”,从Map集合方法元素时,只要指定了Key就能找到相对应的Value
Map接口的常用方法:
方法声明 | 功能描述 |
---|
void put(Object key, Object value) | 将指定的值与此映射中的指定键关联(可选操作) | Object get(Object key) | 返回指定键所映射的值;如果此映射不包含该键的映射关系,则返回null | void clear() | 移除所有的键值对元素 | V remove(Object key) | 根据键删除对应的值,返回被删除的值 | int size() | 返回集合中的键值对的个数 | boolean containsKey(Object key) | 如果此映射包含指定键的映射关系,则返回 true。 | boolean containsValue(Object value) | 如果此映射将一个或多个键映射到指定值,则返回 true | Set keySet() | 返回此映射中包含的键的 Set 视图 | Collection values() | 返回此映射中包含的值的 Collection 视图 | Set<Map.Entry<K,V>>entrySet() | 返回此映射中包含的映射关系的Set视图 |
4.2.1 HashMap集合
HashMap集合是Map接口的一个实现类,用于存储键值映射关系,但HashMap集合没有重复的键且键值无序
import java.util.*;
public class Example14 {
public static void main(String[] args) {
HashMap map = new HashMap();
map.put("1", "张三");
map.put("2", "李四");
map.put("3", "王五");
System.out.println("1:" + map.get("1"));
System.out.println("2:" + map.get("2"));
System.out.println("3:" + map.get("3"));
}
}
Map中的键必须是唯一的,不能重复,如果存储了相同的键,后存储的值则会覆盖原有的值,简而言之就是:键相同,值覆盖
HashMap的存储方式与HashSet略有相似,即通过元素的哈希值来实现无重复元素
HashMap集合的遍历方法:
- 先遍历所有的键再根据键获取相应的值
public class example{
public static void main(String args[]){
HashMap hm = new HashMap();
hm.put("1","张三");
hm.put("2","李四");
hm.put("3","王五");
Set keySet = hm.keySet();
Iterator it = keySet.iterator();
while(it.hasNext()){
Object key = it.next();
Object value = hm.get(key);
System.out.println(key+":"+value);
}
}
}
**要点1:**Set keySet = hm.keySet() 即调用了Map接口的keySet方法,keySet()方法可以返回此映射中键的Set视图
**要点2:**通过迭代器Iterator迭代Set集合中的每一个元素(即每一个键),再通过get(String key)方法,根据键获取对应的值
- 先获取集合中的所有映射关系,然后从映射关系中取出键和值
public class Example{
public static void main(String args[]){
HashMap hm = new HashMap();
hm.put("1","张三");
hm.put("2","李四");
hm.put("3","王五");
Set entrySet = hm.entrySet();
Iterator it = entrySet.iterator();
while(it.hasNext()){
Map.Entry entry = (Map.entry)(it.next());
Object key = entry.getKey();
Object value = entry.getValue(key);
System.out.println(key+":"+value);
}
}
}
要点1: Set entrySet = hm.entrySet(); 即调用entrySet()方法获取存储再Map中所有映射的Set集合,这个集合中存放了Map.Entry类型的元素(Entry是Map内部接口),每个Map.Entry对象代表Map中的一个键值对
**要点2:**通过迭代器lterator迭代Set集合,获得每一个映射对象,并分别调用映射对象的getKey() 和 getValue()方法获取键和值
在Map集合中,还提供了一些操作集合的常用方法:
- values()方法用于得到map实例中所有的value,返回值类型为Collection
- size()方法获取Map集合类的大小
- containsKey()方法用于判断是否包含传入的键
- containsValue()方法用于判断是否包含传入的值
- remove()方法用于根据key移除map实例中与该key对应的value
案例:
import java.util.*;
public class Example17 {
public static void main(String[] args) {
HashMap map = new HashMap(); // 创建Map集合
map.put("1", "张三"); // 存储键和值
map.put("3", "李四");
map.put("2", "王五");
map.put("4", "赵六");
System.out.println("集合大小为:"+map.size());
System.out.println("判断是否包含传入的键:"+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集合迭代出来的元素与存入的顺序并不一致:
如果想让存取顺序一致,可以使用Java中提供的LinkedHashMap类,它是HashMap的子类,与LinkedList一样,也是用双向循环链表来维护内部元素的关系,使Map元素的存取顺序一致
LinkedHashMap实例:
1 import java.util.*;
2 public class Example18 {
3 public static void main(String[] args) {
4 LinkedHashMap map = new LinkedHashMap();
5 map.put("3", "李四");
6 map.put("2", "王五");
7 map.put("4", "赵六");
8 Set keySet = map.keySet();
9 Iterator it = keySet.iterator();
10 while (it.hasNext()) {
11 Object key = it.next();
12 Object value = map.get(key);
13 System.out.println(key + ":" + value);
14 }
15 }
16 }
使用了先遍历所有的键,再根据键迭代值的迭代集合的方法
4.2.2 TreeMap集合
HashMap集合存储的元素的键值是无序的和不可重复的,为了对集合中的元素的键值进行排序,Map接口提供了另一个可以对集合中元素键值进行排序的类TreeMap
案例:
import java.util.Iterator;
import java.util.Set;
import java.util.TreeMap;
public class Example19 {
public static void main(String[] args) {
TreeMap map = new TreeMap();
map.put(3, "李四");
map.put(2, "王五");
map.put(4, "赵六");
map.put(3, "张三");
Set keySet = map.keySet();
Iterator it = keySet.iterator();
while (it.hasNext()) {
Object key = it.next();
Object value = map.get(key);
System.out.println(key+":"+value);
}
}
}
**要点1:**使用了 Set keySet = map.keySet(); 方法先获得所有的键,再通过键获得对应的值
要点2:使用Map里集合类的put()方法添加元素,但添加了两个键为“3”的值,第二个直接覆盖了第一个键为“3”的值,说明TreeMap中的键必须是唯一的,不能重复且有序
TreeMap对键值排序的机理:(TreeMap的排序与TreeSet一样也分自然排序和比较排序)
TreeMap的比较排序实例:
import java.util.*;
class Student {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Student(String name, int age) {
super();
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student [name=" + name + ", age=" + age + "]";
}
}
public class Example20 {
public static void main(String[] args) {
TreeMap tm = new TreeMap(new Comparator<Student>() {
@Override
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);
}
}
}
**要点1:**通过匿名内部类的方式实现了Comparator接口,然后重写了compare()方法,在compare()方法中通过三目运算符的方式自定义了排序方式为先按照年龄排序,年龄相同再按照姓名排序
**要点2:**使用了 Set keySet = map.keySet(); 方法先获得所有的键,再通过键获得对应的值
4.2.3 Properties集合
Map接口还有一个实现类Hashtable,它与HashMap十分相似,区别在于Hashtable是线程安全的
(回顾字符串类StringBuffer也是线程安全的,而StringBuild相对于StringBuffer来说不安全)
Hashtable存取元素时的速度很慢,目前基本被HashMap类所取代,但Hashtable有一个重要的子类Properties在实际开发中非常重要
-
Properties主要用来存储字符串类型的键和值 -
在实际开发中,经常使用Properties集合来存取应用的配置项。假设有一个文本编辑工具,要求默认背景色是红色,字体大小为14px,语言为中文,其配置项如下面的代码: Backgroup-color = red
Font-size = 14px
Language = chinese
实例:
public class Example{
public static void main(String args[]){
Properties p = new Properties();
p.setProperty("Background-color","red")
p.setProperty("Font-size","14px");
p.setProperty("Language","chinese");
Enumeration names = p.propertyNames();
while(names.hasMoreElements()){
String key = (String)names.nextElement();
String value = p.getProperty(key);
System.out.println(key+"="+value);
}
}
}
**要点1:**在上述的Properties类中,针对字符串的存取提供了两个专用的方法setProperty()和setProperty()
**要点2:**Properties类的propertyNames()方法可以得到一个包含所有键的Enumeration对象name中,然后遍历所有的键时,通过调用geyProperty()方法获得键对应的值
4.3.1 泛型概述
泛型是指定一个表示类型的变量,即“参数化类型”,在编程中使用泛型来代替某个实际的类型,而后通过实际调用时传入或推导的类型来对泛型进行替换,以达到代码复用的效果
在使用泛型的过程中,操作数据类型被指定为一个参数,这种参数类型在类,接口和方法中分别称为泛型类,泛型接口,泛型方法
泛型的使用
public class Box<T>{
private T t;
public void set(T t){
this.t=t;
}
public T get(){
return t;
}
}
Box类在定义时使用了“”的形式,T表示此类型是由外部调用本类时指定的。这样,在实例化类对象时可以传入除基础数据类型以外的任意类型数据,使类具有良好的通用性
泛型类:
[访问权限] class 类名称<泛型类型标识1,泛型类型标识2,…,泛型类型标识n> [访问权限] 泛型类型标识 变量名称; [访问权限] 泛型类型标识 方法名称; [访问权限] 返回值类型声明 方法名称(泛型类型标识 变量名称){};
泛型对象:
类名称<参数化类型> 对象名称 = new 类名称<参数化类型>();
实例:
import java.util.*;
public class Example23 {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<Integer>();
list.add(1);
list.add(2);
for (Integer str : list) {
System.out.println(str);
}
}
}
**要点1:**使用泛型规定了ArrayList集合只能存入Integer类型元素,然后向集合中存入了两个Integer类型元素,并对这个集合进行foreach遍历
**要点2:**每次遍历集合元素时,可以指定元素类型为Integer,而不是Object,这样就避免了在程序中进行强制类型转换
泛型方法:
[访问权限] <泛型标识> 返回值类型 方法名称(泛型标识 参数名称)
1 public class Example24 {
2 public static void main(String[] args) {
3
4 Dog dog = new Dog();
5
6 dog.show("hello");
7 dog.show(12);
8 dog.show(12.5);
9 }
10 }
11 class Dog{
12 String eat;
13 Integer age;
14 public <T> void show(T t) {
15 System.out.println(t);
16 }
17 }
18 tool.show(12.5);
19 }
**要点:**定义了一个泛型方法show(),并将show()方法的参数类型和返回值类型规定为泛型,这样调用方法时,传入的参数是什么类型,返回值就是什么类型
泛型接口:
[访问权限] interface 接口名称<泛型标识> {}
泛型接口定义完成之后,就要定义此接口的子类,定义泛型接口的子类有两种方式
当子类明确泛型类的类型参数变量时,外界使用子类的时候,需要传递类型参数变量进来,在实现类中需要定义出类型参数变量
public interface Inter<T> {
public abstract void show(T t);
}
public class InterImpl implements Inter<String> {
@Override
public void show(String s) {
System.out.println(s);
}
}
public class Example25 {
public static void main(String[] args) {
Inter<String> inter = new InterImpl();
inter.show("hello");
}
}
- **要点1:**定义了一个泛型接口Inter,在泛型接口子类InterImpl中实现了Inter接口,Inter inter = new InterImpl();
- **要点2:**InterImpl实现 Inter接口时,直接在实现的接口处制定了具体的泛型类型String,这样在重写Inter接口中的show()方法时直接指明类型为String即可
当子类不明确泛型类的类型参数变量,外界使用子类的时候,也需要传递类型参数变量进来,在实现类中也需要定义出类型参数变。接下来通过修改子类InterImpl和测试程序来学习这种情况的泛型接口定义
public interface Inter<T> {
public abstract void show(T t);
}
public class InterImpl<T> implements Inter<T> {
@Override
public void show(T t) {
System.out.println(t);
}
}
public class Example25 {
public static void main(String[] args) {
Inter<String> inter = new InterImpl();
inter.show("hello");
Inter<Integer> ii = new InterImpl<>();
ii.show(12);
}
}
4.4 类型通配符
在Java中,数组是可以协变的,例如,Dog extends Animal,那么Animal[]与dog[]是可以兼容的。而集合是不能协变的,也就是说List不是List的父类, 为了解决这个问题,Java泛型为我们提供了类型通配符 ?
实例:
import java.util.*;
public class Example28 {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
test(list);
}
public static void test(List<?> list) {
for(int i=0;i<list.size();i++){
System.out.println(list.get(i));
}
}
}
需要注意的是,如果使用通配符“?”接收泛型对象,则通配符“?”修饰的对象只能接收,不能修改,也就是不能设置。错误的代码如下所示
class Test {
public static void main(String[] args) {
List<?> list = new ArrayList<String>();
list.add("张三");
}
}
通配符表示可以匹配任意类型,任意的Java类都可以匹配, 但是当接收一个List集合时,它只能操作数字类型的元素(Float、Integer、Double、Byte等数字类型都行),而如果直接使用通配符的话,该集合就不是只能操作数字了。针对这类问题我们可以设定通配符的上限和下限
- 设定通配符上限代码如下所示:
List<? extends Number> - 设定通配符下限代码如下所示:
<? super Type>
第五重 出窍(I/O输入输出)
5.1 Feil类
Feil类简介:File类中java.io包中唯一代表磁盘文件本身的对象,它定义了一些与平台无关的方法用于操作文件。通过调用File类提供的各种方法,能够创建,删除或者重命名文件,判断硬盘上某个文件是否存在,查询文件最后修改时间等
5.1.1 File对象的创建
File类提供了专门创建File对象的构造方法
方法声明 | 功能描述 |
---|
File(String pathname) | 通过指定的一个字符串类型的文件路径来创建一个新的File对象 | File(String parent,String child) | 根据指定的一个字符串类型的父路径和一个字符串类型的子路径(包括文件名称)创建一个File对象 | File(File parent,String child) | 根据指定的File类的父路径和字符串类型的子路径(包括文件名称)创建一个File对象 |
- 如果程序只处理一个目录或文件,并且知道该目录或文件的路径,使用第一个构造方法较方便
- 如果程序处理的是一个公共目录中的若干子目录或文件,使用第二,第三个构造方法会更方便
5.1.2 File类的常用方法
File类提供了一系列方法,用于操作其内部封装的路径指向的文件或者目录
方法声明 | 功能描述 |
---|
boolean exists() | 判断File对象对应的文件或目录是否存在,若存在则返回ture,否则返回false | boolean delete() | 删除File对象对应的文件或目录,若成功删除则返回true,否则返回false | boolean createNewFile() | 当File对象对应的文件不存在时,该方法将新建一个此File对象所指定的新文件,若创建成功则返回true,否则返回false | String getName() | 返回File对象表示的文件或文件夹的名称 | String getPath() | 返回File对象对应的路径 | String getAbsolutePath() | 返回File对象对应的绝对路径(在Unix/Linux等系统上,如果路径是以正斜线/开始,则这个路径是绝对路径;在Windows等系统上,如果路径是从盘符开始,则这个路径是绝对路径) | String getParentFile() | 返回File对象对应目录的父目录(即返回的目录不包含最后一级子目录) |
方法声明 | 功能描述 |
---|
boolean canRead() | 判断File对象对应的文件或目录是否可读,若可读则返回true,反之返回false | boolean canWrite() | 判断File对象对应的文件或目录是否可写,若可写则返回true,反之返回false | boolean isFile() | 判断File对象对应的是否是文件(不是目录),若是文件则返回true,反之返回false | boolean isDirectory() | 判断File对象对应的是否是目录(不是文件),若是目录则返回true,反之返回false | boolean isAbsolute() | 判断File对象对应的文件或目录是否是绝对路径 | long lastModified() | 返回1970年1月1日0时0分0秒到文件最后修改时间的毫秒值; | long length() | 返回文件内容的长度 | String[] list() | 列出指定目录的全部内容,只是列出名称 | File[] listFiles() | 返回一个包含了File对象所有子文件和子目录的File数组 |
实例:
package Seven;
import java.io.IOException;
import java.io.File;
class io{
public static void main(String []args) throws IOException{
File file = new File("E:\\hello\\demo.txt");
if(file.exists()){
file.delete();
}else{
System.out.println(file.createNewFile());
}
File fileDemo = new File("E:\\hello1\\program");
if(!(fileDemo.getParentFile().exists())){
fileDemo.getParentFile().mkdir();
}
if(fileDemo.exists()){
fileDemo.delete();
}else{
System.out.println(fileDemo.createNewFile());
}
}
}
createTempFile()方法和deleteOnExit()方法:
在一些特定情况下,程序需要读写一些临时文件,File对象提供了createTempFile()来创建一个临时文件,以及deleteOnExit()在Jvm退出时自动删除该文件
实例:
public class Example{
public static void main(String arsg[]){
File f = File.createTempFile("itcast-",".txt");
f.deleteOnExit();
System.out.println(f.isFile());
System.out.println(f.getPath());
}
}
5.1.3 遍历目录下的文件
File类的list()方法用于遍历指定目录下的所有文件
实例:
public class Example{
public static void main(String args[]){
File file = new File("D:/scape/chapter");
if(!file.isDirectory()){
String[] names = file.list
for(String name:names){
System.out.println(name);
}
}
}
}
有时候程序只是需要得到指定类型的文件.如获取指定目录下所有的".java"文件,针对程序的这种需求,File类提供了一个重载的list(FilenameFilter filter)方法
该方法接收一个FilenameFilter类型的参数(FilenameFilter是一个接口,被称为文件过滤器,当中定义了一个抽象方法accept(File dir,String name)),在调用list()方法时,需要实现文件过滤器FilenameFilter,并在accept()方法中做出判断,从而获得指定类型文件
list(FilenameFilter filter)方法的工作原理:
- 調用list()方法傳入FilenameFilter文件過濾器
- 取出當前File對象所代表目錄下的所有子目錄和文件
- 對於每一個子目錄或文件,都會調用文件過濾器的accept(File dir,String name)方法,并把代表當前目錄的File文件以及這個子目錄或文件的名字作爲參數dir和name傳入方法
- 如果accept()方法返回true,就將遍歷這個子目錄或文件添加到數組中,如果返回false則不添加
實例:
import java.io.File;
import java.io.FilenameFilter;
public class Example{
public static void main(String args[]) throws Exception{
File file = new File("E:/MarkDown/hello.md");
FilenameFilter filter = new FilenameFilter(){
public boolean accept(File dir,String name){
File currFile = new File(dir,name);
if(currFile.isFile() && name.endsWith(".md")){
return true;
}else{
return false;
}
}
};
if(file.exists()){
String[] lists = file.list(filter);
for(String names:lists){
System.out.println(names);
}
}
}
}
有時候在一個目錄下,除了文件還有子目錄,如果想得到所有子目錄下的File類型對象,list()方法并不適用,這時需要使用File類提供的另一個方法listFiles(),listFiles()方法返回一個File數組對象,黨對數組中的元素進行遍歷時如果目錄中還有子目錄,則需要使用遞歸
實例:
import java.io.File;
public class Example{
public static void main(String []args){
File file = new File("E/MarkDown");
fileDir(file);
}
public static void fileDir(File dir){
File file[] = dir.listFiles();
for(File files:file){
if(file.isDirectory()){
fileDir(file);
}
Syetem.out.println(file.getAbsolutePath());
}
}
}
5.1.4 刪除文件及目錄
在操作文件时,我们会遇到需要删除一个目录下的某个文件或者删除整个目录,这时需要使用到File的delete()方法
實例:
import java.io.*;
public class Example06 {
public static void main(String[] args) {
File file = new File("D:\\hello\\test");
if (file.exists()) {
System.out.println(file.delete());
}
}
}
运行结果中输出了false,这说明删除文件失败了。原因是File类的delete()方法只能删除一个指定的文件,假如File对象代表目录,并且目录下包含子目录或文件,则File类的delete()方法不允许对这个目录直接删除。在这种情况下,需要通过递归的方式将整个目录以及其中的文件全部删除
實例:
import java.io.*;
public class Example{
public static void main(String[] args){
File file = new File("E:/hello");
deleteDir(file);
}
public void deleteDir(File file){
if(dir.exists()){
File[] files = dir.listFiles();
for(File file:files){
if(file.isDirectory()){
deleteDir(file);
}else{
file.delete(); }
}
dir.delete();
}
}
}
删除目录是从虚拟机直接删除而不放入回收站的,文件一旦删除就无法恢复,因此在进行删除操作的时候需要格外小心
5.2 字节流
5.2.1 字節流的概念
在程序的开发中,我们经常会需要处理设备之间的数据传输,而计算机中,无论是文本、图片、音频还是视频,所有文件都是以二进制(字节)形式存在的
而对于字节的输入输出IO流提供了一系列的流,统称为字节流,字节流是程序中最常用的流,根据数据的传输方向可将其分为字节输入流和字节输出流
InputStream 和 OutputStream:
在JDK中,提供了兩個抽象類InputStream 和 OutputStream,他們是字節流的頂級父類,所有的字節輸入都繼承自InputStream,所有的字節輸出都繼承自OutputStream
为了方便理解,可以把InputStream和OutputStream比作两根“水管”
InputStream被看成一个输入管道,OutputStream被看成一个输出管道,数据通过InputStream从源设备输入到程序,通过OutputStream从程序输出到目标设备,从而实现数据的传输,由此可见,IO流中的输入输出都是相对于程序而言的
在JDK中,InputStream和 OutputStream提供了一系列与读写数据相关的方法
InputStream:
方法声明 | 功能描述 |
---|
int read() | 从输入流读取一个8位的字节,把它转换为0~255之间的整数,并返回这一整数 | int read(byte[] b) | 从输入流读取若干字节,把它们保存到参数b指定的字节数组中,返回的整数表示读取字节的数目 | int read(byte[] b,int off,int len) | 从输入流读取若干字节,把它们保存到参数b指定的字节数组中,off指定字节数组开始保存数据的起始下标,len表示读取的字节数目 | void close() | 关闭此输入流并释放与该流关联的所有系统资源 |
OutputStream:
方法名称 | 方法描述 |
---|
void write(int b) | 向输出流写入一个字节 | void write(byte[] b) | 把参数b指定的字节数组的所有字节写到输出流 | void write(byte[] b,int off,int len) | 将指定byte数组中从偏移量off开始的len个字节写入输出流 | void flush() | 刷新此输出流并强制写出所有缓冲的输出字节 | void close() | 关闭此输出流并释放与此流相关的所有系统资源 |
InputStream和OutputStream这两个类虽然提供了一系列和读写数据有关的方法,但是这两个类是抽象类,不能被实例化,因此,针对不同的功能,InputStream和OutputStream提供了不同的子类,这些子类形成了一个体系结构
InputStream的子類:
OutputStream的子類:
5.2.2 InputStream讀文件
InputStream就是JDk提供的基本输入流。但InputStream并不是一个接口,而是一个抽象类,它是所有输入流的父类,而FileInputStream是InputStream的子类,它是操作文件的字节输入流,专门用于读取文件中的数据。由于从文件读取数据是重复的操作,因此需要通过循环语句来实现数据的持续读取
FileInputStream讀取文件實例:
import java.io.*;
public class Example08 {
public static void main(String[] args) throws Exception {
FileInputStream in = new FileInputStream("test.txt");
int b = 0;
while (true) {
b = in.read();
if (b == -1) {
break;
}
System.out.println(b);
}
in.close();
}
}
字符‘i’、‘t’、‘c’、‘a’、‘s’、‘t’各占一个字节,因此,最终结果显示的就是文件“test.txt”中的六个字节所对应的十进制数
有时,在文件读取的过程中可能会发生错误。例如,文件不存在导致无法读取,没有读取权限等,这些错都是由Java虚拟机自动封装成IOException异常并抛出
文件不存在时控制台的报错信息对于上述异常错误,会有一个潜在的问题,如果读取过程中发生了IO错误,InputStream就无法正常关闭,资源也无法及时释放。对于这种问题我们可以使用try…finally来保证InputStream在无论是否发生IO错误的时候都能够正确关闭
實例:
import java.io.FileputStream;
import java.io.InputStream;
public class Example{
public staic void main(String args[]){
InputStream input = null;
try{
FileInputStream in = new FileInputStream("test.txt");
int b = 0;
b = in.read();
if(b==-1){
break;
}
System.out.println(b);
}
finally{
if(input != null){
input.close();
}
}
}
}
5.2.3 OnputStream寫文件
OutputStream是JDK提供的最基本的输出流,与InputStream类似的是OutputStream也是抽象类,它是所有输出流的父类
OutputStream是一个抽象类,如果使用此类,则首先必须通过子类实例化对象。FileOutputStream是OutputStream的子类,它是操作文件的字节输出流,专门用于把数据写入文件
實例:
public static void main(String []args){
OutputStream out = new FileOutputStream("example.txt");
String str = "皮爾特沃夫";
byte[] b = str.getBytes();
for(int i=0;i<b.length;i++){
out.write(b[i]);
}
}
注意:如果是通过FileOutputStream向一个已经存在的文件中写入数据,那么该文件中的数据首先会被清空,再写入新的数据。若希望在已存在的文件内容之后追加新内容,则可使用FileOutputStream的构造函数
FileOutputStream(String fileName, boolean append)来创建文件输出流对象,并把append 参数的值设置为true
實例:
public static void main(String[] args){
OutputStream out = new FileOutputStream("example.txt",true);
String str = "--伊澤瑞爾";
byte[] b = str.getBytes();
for(int i=0li<b.length;i++){
out.write(b[i]);
}
out.close;
}
由于IO流在进行数据读写操作时会出现异常,为了代码的简洁,在上面的程序中使用了throws关键字将异常抛出。然而一旦遇到IO异常,IO流的close()方法将无法得到执行,流对象所占用的系统资源将得不到释放
因此,为了保证IO流的close()方法必须执行,通常将关闭流的操作写在finally代码块中
寫法:
finally{
try{
if(in!=null) in.close();
}catch(Exception e){
e.printStackTrace();
}
try{
if(out!=null) out.close();
}catch(Exception e){
e.printStackTrace();
}
}
5.2.4 文件的拷貝
在应用程序中,IO流通常都是成对出现的,即输入流和输出流一起使用。例如,文件的拷贝就需要通过输入流来读取文件中的数据,通过输出流将数据写入文件
文件拷貝實例:
public static void main(String[]args){
InputStream in = new FileInputStream("E:/hello1/demo.txt");
OutputStream out = new FileOutputStream("E:/hello2/demo2.txt");
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();
}
上述实现的文件拷贝是一个字节一个字节的读写,需要频繁的操作文件,效率非常低
在拷贝文件时,可以一次性读取多个字节的数据,并保存在字节数组中,然后将字节数组中的数据一次性写入文件
實例:
import java.io.*;
public class Example13{
public static void main(String[] args) throws Exception {
InputStream in = new FileInputStream("source/五环之歌.doc");
OutputStream out = new FileOutputStream("target/五环之歌.doc");
byte[] buff = new byte[1024];
int len;
long begintime = System.currentTimeMillis();
while ((len = in.read(buff)) != -1) {
out.write(buff, 0, len);
}
long endtime = System.currentTimeMillis();
System.out.println("拷贝文件所消耗的时间是:" + (endtime - begintime) +
"毫秒");
in.close();
out.close();
}
}
5.2.5 字節緩衝流
IO提供两个带缓冲的字节流,分别是BufferedInputStream和BufferedOutputStream,它们的构造方法中分别接收InputStream和OutputStream类型的参数作为对象,在读写数据时提供缓冲功能。应用程序、缓冲流和底层字节流之间的关系如下图
通過字節緩衝流拷貝文件:
public static void main(String[] args){
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("E:/hello/demo.txt));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("E:/hello3/demo.txt"));
int len;
while((len=bis.read())!=-1){
bos.write(len);
}
bis.close();
bos.close();
}
创建了BufferedInputStream和BufferedOutputStream两个缓冲流对象,这两个流内部都定义了一个大小为8192的字节数组,首先将读写的数据存入定义好的字节数组,然后将字节数组的数据一次性读写到文件中
(沒有使用帶緩衝區的字節輸入輸出流是將字節取一個存一個,而緩衝區會將字節全部去除,在一次性存入)
5.3 字节流
字符流簡介:InputStream类和OutputStream类在读写文件时操作的都是字节,如果希望在程序中操作字符,使用这两个类就不太方便,为此JDK提供了字符流。同字节流一样,字符流也有两个抽象的顶级父类,分别是Reader和Writer。其中Reader是字符输入流,用于从某个源设备读取字符。Writer是字符输出流,用于向某个目标设备写入字符
5.3.1 字符流定義及其基本用法
字符流的继承关系与字节流的继承关系有些类似,很多子类都是成对(输入流和输出流)出现的
FileReader實例:
import java.io.*;
public class Example15 {
public static void main(String[] args) throws Exception {
FileReader reader = new FileReader("reader.txt");
int ch;
while ((ch = reader.read()) != -1) {
System.out.println((char) ch);
}
reader.close();
}
}
需要注意的是,字符输入流的read()方法返回的是int类型的值,如果想获得字符就需要进行强制类型转换
FileReader对象返回的字符流是char而InputStream对象返回的字符流是byte这就是两者之间最大的区别
FileWriter實例:
import java.io.*;
public class Example16 {
public static void main(String[] args) throws Exception {
FileWriter writer = new FileWriter("writer.txt");
String str = "你好,传智播客";
writer.write(str);
writer.write("\r\n");
writer.close();
}
}
5.3.2 字符流操作文件
需要注意的是,在BufferedReader中有一个重要的方法readLine(),该方法用于一次读取一行文本
字符流文件字符追加:
FileWriter writer = new FileWriter(“writer.txt”,true);
文件拷貝:
public static void main(String[] args){
BufferedReader br = new BufferedReader(new FileReader("E:/world/demo.txt"));
BufferedWriter bw = new BufferedWriter(new FileWriter("E:/world2/demo2.txt"));
String str;
while((str=br.readLine())!=null){
bw.writer(str);
bw.newLine();
}
br.close();
bw.close();
}
注意:
由于字符缓冲流内部使用了缓冲区,在循环中调用BufferedWriter的write()方法写入字符时,这些字符首先会被写入缓冲区,当缓冲区写满时或调用close()方法时,缓冲区中的字符才会被写入目标文件
因此在循环结束时一定要调用close()方法,否则极有可能会导致部分存在缓冲区中的数据没有被写入目标文件
5.3.3 轉換流
前面提到IO流可分为字节流和字符流,有时字节流和字符流之间也需要进行转换。在JDK中提供了两个类可以将字节流转换为字符流,它们分别是InputStreamReader和OutputStreamWriter
InputStreamReader是Reader的子类,它可以将一个字节输入流转换成字符输入流,方便直接读取字符。OutputStreamWriter是Writer的子类,它可以将一个字节输出流转换成字符输出流,方便直接写入字符。通过转换流进行数据读写的过程如下图
實例:
import java.io.*;
public class Example18 {
public static void main(String[] args) throws Exception {
FileInputStream in = new FileInputStream("src.txt");
InputStreamReader isr = new InputStreamReader(in);
BufferedReader br = new BufferedReader(isr);
FileOutputStream out = new FileOutputStream("des.txt");
OutputStreamWriter osw = new OutputStreamWriter(out);
BufferedWriter bw = new BufferedWriter(osw);
String line;
while ((line = br.readLine()) != null) {
bw.write(line);
}
br.close();
bw.close();
}
}
上述代码实现了字节流和字符流之间的转换,将字节流转换为字符流,从而实现直接对字符的读写
需要注意的是,在使用转换流时,只能针对操作文本文件的字节流进行转换**,如果字节流操作的是一张图片,此时转换为字符流就会造成数据丢失**
第六重 合体(Java多线程)
6.1 綫程概述
計算機能夠同時完成多項任務,這就是多綫程技術,計算機的CPU即使是單核,也可以同時運行多個任務,因爲操作系統執行多個任務時就是讓CPU對多個任務輪流交替執行
Java是支持多线程的语言之一,它内置了对多线程技术的支持,可以使程序同时执行多个执行片段
6.1.1 進程
進程概述:在一个操作系统中,每个独立执行的程序都可称之为一个进程,也就是“正在运行的程序”
在多任务操作系统中,表面上是支持进程并发执行的,但实际上这些进程并不是同时运行的。在计算机中,所有的应用程序都是由CPU执行的,对于一个CPU而言,在某个时间点只能运行一个程序,也就是说只能执行一个进程。操作系统会为每一个进程分配一段有限的CPU使用时间,CPU在这段时间中执行某个进程,然后会在下一段时间切换到另一个进程中去执行。由于CPU运行速度很快,能在极短的时间内在不同的进程之间进行切换,所以给人以同时执行多个程序的感觉
6.1.2 线程
每个运行的程序都是一个进程,在一个进程中还可以有多个执行单元同时运行,这些执行单元可以看作程序执行的一条条线索,被称为线程
操作系统中的每一个进程中都至少存在一个线程,进程与线程是包含于被包含的关系
Java程序启动时,就会产生一个进程,该进程中会默认创建一个线程,在这个线程上会运行main()方法中的代码
代码按照调用顺序依次往下进行,没有出现两段程序代码交替运行的效果,这样的程序就是单线程程序,如果希望程序实现多段程序代码交替运行的效果,则需要创建多个线程,即多线程程序
多线程:所谓的多线程是指一个进程在执行过程中可以产生多个单线程,这些单线程程序在运行时是相互独立的,它们可以并发执行,多线程与进程相似,看似是多个线程同时执行,其实也是由CPU轮流执行的
进程与线程虽然是包含关系,但是多任务即可以由多线程实现,也可以由单进程的多线程实现,还可以混合多线程、多进程,具体采用哪种方式,还要考虑到进程和线程的特点
多进程与多线程:
多进程的优点:与多线程相比,多进程的稳定性更高,因为在多进程的情况下进程崩溃不会影响其他进程,而在多线程的情况下,任何一个线程崩溃都会直接导致整个进程崩溃
多进程的缺点:
- 创建进程的开销比创建线程的大,尤其在Windows系统上
- 进程间通信比线程间通信缓慢,因为线程间通信就是读写同一个变量,速度很快
6.2 綫程的創建
Java中提供了两种多线程实现方式
- 一种是继承java.lang包下的Thread类,覆写Thread类的run()方法,在run()方法中实现运行在线程上的代码
- 另一种是实现java.lang.Runnable接口,同样是在run()方法中实现运行在线程上的代码
6.2.1 继承Thread类创建多线程
如果希望上面的程序中的两个while循环都能够执行,就需要实现多线程
java提供了一个线程类Thread类,通过继承这个类,并重写run方法便可实现多线程,Thread类也提供了一个start方法用于启动新线程,线程启动后虚拟机会自动调用run方法
实例:
public class Example02 {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
while (true) {
System.out.println("main()方法在运行");
}
}
}
class MyThread extends Thread {
public void run() {
while (true) {
System.out.println("MyThread类的run()方法在运行");
}
}
}
从运行结果,可以看到两个循环中的语句都有输出,说明该文件实现了多线程
多线程与多线程的区别:
单线程的程序在运行时,会按照代码的调用顺序执行,而在多线程中,main()方法和MyThread类的run()方法却可以同时运行,互不影响,这正是单线程和多线程的区别
6.2.2 实现Runnable接口创建多线程
通过继承Thread类可以实现多线程,但是这种方式有一定的局限性。因为Java只支持单继承,一个类一旦继承了某个父类就无法再继承Thread类,比如学生类Student继承了Person类,就无法通过继承Thread类创建线程
为了克服这种弊端,Thread类提供了另外一个构造方法Thread(Runnable target),其中Runnable是一个接口,它只有一个run方法,当通过Thread(Runnable target)
创建线程对象时,只需为该方法传递一个实现了Runnable接口的实例化对象,这样创建的线程将调用实现了Runnable接口的类中的run()方法作为运行代码,而不需要调用Thread类中的run()方法
实例:
public class Example03 {
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();
while (true) {
System.out.println("main()方法在运行");
}
}
}
class MyThread implements Runnable {
public void run() {
while (true) {
System.out.println("MyThread类的run()方法在运行");
}
}
}
通过应用场景分析:
假设售票厅有四个窗口可发售某日某次列车的100张车票,这时,100张车票可以看做共享资源,四个售票窗口需要创建四个线程。为了更直观显示窗口的售票情况,可以通过Thread的currentThread()方法得到当前的线程的实例对象,然后调用getName()方法可以获取到线程的名称
public class Example04 {
public static void main(String[] args) {
new TicketWindow().start();
new TicketWindow().start();
new TicketWindow().start();
new TicketWindow().start();
}
}
class TicketWindow extends Thread {
private int tickets = 100;
public void run() {
while (true) {
if (tickets > 0) {
Thread th = Thread.currentThread();
String th_name = th.getName();
System.out.println(th_name + " 正在发售第 " + tickets-- + " 张票 ");
}
}
}
}
从运行结果可以看出,每张票都被打印了四次。出现这样现象的原因是四个线程没有共享100张票,而是各自出售了100张票。在程序中创建了四个TicketWindow对象,就等于创建了四个售票程序,每个程序中都有100张票,每个线程在独立地处理各自的资源。需要注意的是,上述程序中每个线程都有自己的名字,主线程默认的名字是“main”,用户创建的第一个线程的名字默认为“Thread-0”,第二个线程的名字默认为“Thread-1”,以此类推。如果希望指定线程的名称,可以通过调用setName(String name)方法为线程设置名称
由于现实中铁路系统的票资源是共享的,因此上面的运行结果显然不合理
为了保证资源共享,在程序中只能创建一个售票对象,然后开启多个线程去运行同一个售票对象的售票方法。简单来说就是四个线程运行同一个售票程序,这时就需要用到多线程的第二种实现方式
public class Example05 {
public static void main(String[] args) {
TicketWindow tw = new TicketWindow();
new Thread(tw, "窗口1").start();
new Thread(tw, "窗口2").start();
new Thread(tw, "窗口3").start();
new Thread(tw, "窗口4").start();
}
}
class TicketWindow implements Runnable {
private int tickets = 100;
public void run() {
while (true) {
if (tickets > 0) {
Thread th = Thread.currentThread();
String th_name = th.getName();
System.out.println(th_name + " 正在发售第 " + tickets-- + " 张票 ");
}
}
}
}
创建了一个TicketWindow对象并实现了Runnable接口,然后在mian方法中创建了四个线程,在每个线程上都去调用这个TicketWindow对象中的run()方法,这样就可以确保四个线程访问的是同一个tickets变量,共享100张车票
(这就是所谓线程间通信访问的是同一个变量)
实现Runnable接口相对于继承Thread接口1来说具有的优势:
- 适合多个相同程序代码的线程去处理同一个资源的情况,把线程同程序代码、数据有效的分离,很好的体现了面向对象语言的设计思想
- 可以避免java的单继承带来的局限性,在开发中时常会碰到一个子类继承于一个父类,由于一个子类不能同时有两个父类,所以不能通过继承Thread类的方式创建多线程,只能采用实现Runnable接口的方式
6.3 綫程的生命周期及狀態轉換
生命周期简介:在Java中,任何对象都有生命周期,线程也不例外,它也有自己的生命周期。当Thread对象创建完成时,线程的生命周期便开始了。当run()方法中代码正常执行完毕或者线程抛出一个未捕获的异常(Exception)或者错误(Error)时,线程的生命周期便会结束
线程整个生命周期可以分为五个阶段,分别是新建状态(New)、就绪状态(Runnable)、运行状态(Running)、阻塞状态(Blocked)和死亡状态(Terminated)
线程的不同状态表明了线程当前正在进行的活动
在上图中,单箭头表示状态只能单向的转换(例如·只能从新建状态转换到就绪状态),双箭头表示两种状态可以互相转换(例如就绪状态和运行状态之间)
线程生命周期中的五种状态:
- 新建状态(New):创建一个线程对象后,该线程对象就处于新建状态,此时它不能运行,和其他Java对象一样,仅仅由Java虚拟机为其分配了内存,没有表现出任何线程的动态特征
- 就绪状态(Runnable):当线程对象调用了start()方法后,该线程就进入就绪状态。处于就绪状态的线程位于线程队列中,此时它只是具备了运行的条件,能否获得CPU的使用权并开始运行,还需要等待系统的调度(等待CPU为其分配时间)
- 运行状态(Running):如果处于就绪状态的线程获得了CPU的使用权,并开始执行run()方法中的线程执行体,则该线程处于运行状态。一个线程启动后,它可能不会一直处于运行状态,当运行状态的线程使用完系统分配的时间后,系统就会剥夺该线程占用的CPU资源,让其他线程获得执行的机会。需要注意的是,只有处于就绪状态的线程才可能转换到运行状态
- 阻塞状态(Blocked):一个正在执行的线程在某些特殊情况下,如被人为挂起或执行耗时的输入/输出操作时,会让出CPU的使用权并暂时中止自己的执行,进入阻塞状态。线程进入阻塞状态后,就不能进入排队队列。只有当引起阻塞的原因被消除后,线程才可以转入就绪状态
- 死亡状态(Terminated):当线程调用stop()方法或run()方法正常执行完毕后,或者线程抛出一个未捕获的异常(Exception)、错误(Error),线程就进入死亡状态。一旦进入死亡状态,线程将不再拥有运行的资格,也不能再转换到其他状态
线程由运行状态传唤成阻塞状态的原因,以及如何从阻塞状态转换成就绪状态:
- 当线程试图获取某个对象的同步锁时,如果该锁被其他线程所持有,则当前线程会进入阻塞状态,如果想从阻塞状态进入就绪状态必须得获取到其他线程所持有的锁
- 当线程调用了一个阻塞式的IO方法时,该线程就会进入阻塞状态,如果想进入就绪状态就必须要等到这个阻塞的IO方法返回
- 当线程调用了某个对象的wait()方法时,也会使线程进入阻塞状态,如果想进入就绪状态就需要使用notify()方法唤醒该线程
- 当线程调用了Thread的sleep(long millis)方法时,也会使线程进入阻塞状态,在这种情况下,只需等到线程睡眠的时间到了以后,线程就会自动进入就绪状态
- 当在一个线程中调用了另一个线程的join()方法时,会使当前线程进入阻塞状态,在这种情况下,需要等到新加入的线程运行结束后才会结束阻塞状态,进入就绪状态
注意:线程从阻塞状态只能进入就绪状态,而不能直接进入运行状态,也就是说结束阻塞的线程需要重新进入可运行池中,等待系统的调度
6.4 綫程的調度
线程调度的两种模式:分时调度模式、抢占式调度模式
- 分时调度模式:指让所有的线程轮流获得CPU的使用权,并且平均分配每个线程占用的CPU的时间片
- 抢占式调度模式:指让可运行池中优先级高的线程优先占用CPU,而对于优先级相同的线程,随机选择一个线程使其占用CPU,当它失去了CPU的使用权后,再随机选择其他线程获取CPU使用权
Java虚拟机默认采用抢占式调度模型,通常情况下程序员不需要去关心它,但在某些特定的需求下需要改变这种模式,由程序自己来控制CPU的调度
6.4.1 线程的优先级
线程优先级简介:在应用程序中,如果要对线程进行调度,最直接的方式就是设置线程的优先级。优先级越高的线程获得CPU执行的机会越大,而优先级越低的线程获得CPU执行的机会越小。线程的优先级用1~10之间的整数来表示,数字越大优先级越高
除了可以直接使用数字表示线程的优先级,还可以使用Thread类中提供的三个静态常量表示线程的优先级
Thread****类的静态常量 | 功能描述 |
---|
static int MAX_PRIORITY | 表示线程的最高优先级,值为10 | static int MIN_PRIORITY | 表示线程的最低优先级,值为1 | static int NORM_PRIORITY | 表示线程的普通优先级,值为5 |
改变线程优先级:main线程具有普通优先级。然而线程优先级不是固定不变的,可以通过Thread类的setPriority(int newPriority)方法进行设置,setPriority()方法中的参数newPriority接收的是1~10之间的整数或者Thread类的三个静态常量
实例:
class MaxPriority implements Runnable{
public void run(){
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"正在输出"+i);
}
}
class MinPriority implements Runnable{
public void run(){
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"正在输出"+i);
}
}
}
public static void main(String[] args){
Thread minPriority = new Thread(new MinPriority(),"优先级较低的线程");
Thread maxPriority = new Thread(new MaxPriority(),"优先级较高的线程");
minPriority.setPriority(Thread.MIN_PRIORITY);
maxPriority.setPriority(Thread.MAX_PRIORITY);
maxPriority.start();
minPriority.start();
}
注意:虽然Java中提供了10个线程优先级,但是这些优先级需要操作系统的支持,不同的操作系统对优先级的支持是不一样的,不会和Java中线程优先级一一对应,因此,在设计多线程应用程序时,其功能的实现一定不能依赖于线程的优先级,而只能把线程优先级作为一种提高程序效率的手段
6.4.2 线程休眠
线程休眠简介:如果希望人为地控制线程,使正在执行的线程暂停,将CPU让给别的线程,这时可以使用静态方法sleep(long millis),该方法可以让当前正在执行的线程暂停一段时间,进入休眠等待状态。当前线程调用sleep(long millis)方法后,在指定时间(单位毫秒)内该线程是不会执行的,这样其他的线程就可以得到执行的机会了
sleep(long millis)方法声明会抛出InterruptedException异常,因此在调用该方法时应该捕获异常,或者声明抛出该异常
实例:
package Eight;
public class test04 {
public static class SleepThread implements Runnable{
@Override
public void run() {
for(int i=1;i<=10;i++) {
if(i==3) {
try {
Thread.sleep(2000);
}catch(InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("SleepThread线程正在输出"+i);
try {
Thread.sleep(500);
}catch(Exception e) {
e.printStackTrace();
}
}
}
}
public static void main(String[]args) throws InterruptedException {
new Thread(new SleepThread()).start();
for(int i=1;i<=10;i++) {
if(i==5) {
Thread.sleep(2000);
}
System.out.println("主线程正在输出"+i);
Thread.sleep(500);
}
}
}
sleep()是静态方法,只能控制当前正在运行的线程休眠,而不能控制其他线程休眠。当休眠时间结束后,线程就会返回到就绪状态,而不是立即开始运行
6.4.3 线程让步
线程让步简介:所谓的线程让步是指正在执行的线程,在某些情况下将CPU资源让给其他线程执行,线程让步可以通过yield()方法来实现,该方法和sleep()方法有点相似,都可以让当前正在运行的线程暂停,区别在于yield()方法不会阻塞该线程,它只是将线程转换成就绪状态,让系统的调度器重新调度一次。当某个线程调用yield()方法之后,只有与当前线程优先级相同或者更高的线程才能获得执行的机会
实例:
1
2 class YieldThread extends Thread {
3
4 public YieldThread(String name) {
5 super(name);
6 }
7 public void run() {
8 for (int i = 0; i < 6; i++) {
9 System.out.println(Thread.currentThread().getName() + "---" + i);
10 if (i == 3) {
11 System.out.print("线程让步:");
12 Thread.yield();
13 }
14 }
15 }
16 }
17 public class Example08 {
18 public static void main(String[] args) {
19
20 Thread t1 = new YieldThread("线程A");
21 Thread t2 = new YieldThread("线程B");
22
23 t1.start();
24 t2.start();
25 }
26 }
6.4.4 线程插队
线程插队简介:在Thread类中也提供了一个join()方法来实现这个“功能”。当在某个线程中调用其他线程的join()方法时,调用的线程将被阻塞,直到被join()方法加入的线程执行完成后它才会继续运行
package Eight;
public class test05 {
public static class EmergencyThread implements Runnable{
@Override
public void run() {
for(int i=1;i<6;i++) {
System.out.println(Thread.currentThread().getName()+"输入"+i);
try {
Thread.sleep(500);
}catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[]args) throws InterruptedException {
Thread t = new Thread(new EmergencyThread(),"紧急线程");
t.start();
for(int i=1;i<6;i++) {
System.out.println(Thread.currentThread().getName()+"输入"+i);
if(i==2) {
t.join();
}
Thread.sleep(500);
}
}
}
在上述代码中,在第4行代码中开启了一个线程t,两个线程的循环体中都调用了Thread的sleep(500)方法,以实现两个线程的交替执行。当main线程中的循环变量为2时,调用t线程的join()方法,这时,t线程就会“插队”优先执行。从运行结果可以看出,当main线程输出2以后,线程一就开始执行,直到线程一执行完毕,main线程才继续执行
6.5 多綫程同步
多线程同步简介:多线程的并发执行可以提高程序的效率,但是,当多个线程去访问同一个资源时,也会引发一些安全问题。例如,当统计一个班级的学生数目时,如果有同学进进出出,则很难统计正确。为了解决这样的问题,需要实现多线程的同步,即限制某个资源在同一时刻只能被一个线程访问
6.5.1 线程安全问题
前面讲解的售票案例,极有可能碰到“意外”情况,如一张票被打印多次,或者打印出的票号为0甚至负数。这些“意外”都是由多线程操作共享资源ticket所导致的线程安全问题。接下来对售票案例进行修改,模拟四个窗口出售10张票,并在售票的代码中使用sleep()方法,令每次售票时线程休眠10毫秒
package Eight;
public class test06 {
public static class SaleThread implements Runnable{
private int tickets = 10;
public void run() {
while(tickets>0) {
try {
Thread.sleep(10);
}catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"---卖出的票:"+tickets--);
}
}
}
public static void main(String[] args) {
SaleThread st = new SaleThread();
new Thread(st,"线程1").start();
new Thread(st,"线程2").start();
new Thread(st,"线程3").start();
new Thread(st,"线程4").start();
}
}
在运行结果中,最后打印售出的票出现了0和负数,这种现象是不应该出现的,因为售票程序中只有当票号大于0时才会进行售票。运行结果中之所以出现了负数的票号是因为多线程在售票时出现了安全问题
安全问题的来源:在售票程序的while循环中添加了sleep()方法,由于线程有延迟,当票号减为1时,假设线程1此时出售1号票,对票号进行判断后,进入while循环,在售票之前通过sleep()方法让线程休眠,这时线程二会进行售票,由于此时票号仍为1,因此线程二也会进入循环,同理,四个线程都会进入while循环,休眠结束后,四个线程都会进行售票,这样就相当于将票号减了四次,结果中出现了0、-1、-2这样的票号
6.5.2 同步代码块
线程安全问题其实就是由多个线程同时处理共享资源所导致的,要想解决线程安全问题,必须得保证在任何时刻只能有一个线程访问共享资源
为了实现这种限制,Java中提供了同步机制。当多个线程使用同一个共享资源时,可以将处理共享资源的代码放在一个使用synchronized关键字修饰的代码块中,这个代码块被称作同步代码块
synchronized(lock){
操作共享资源代码块
}
上面的格式中,lock是一个锁对象,它是同步代码块的关键。当某一个线程执行同步代码块时,其他线程将无法执行当前同步代码块,会发生阻塞,等当前线程执行完同步代码块后,所有的线程开始抢夺线程的执行权,抢到执行权的线程将进入同步代码块,执行其中的代码。循环往复,直到共享资源被处理完为止。这个过程就好比一个公用电话亭,只有前一个人打完电话出来后,后面的人才可以打
同步代码块的锁是自己定义的任意类型的对象
实例:
class Ticket1 implements Runnable {
private int tickets = 10;
Object lock = new Object();
public void run() {6 while (true) {
synchronized (lock) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (tickets > 0) {
System.out.println(Thread.currentThread().getName()
+ "---卖出的票" + tickets--);
} else {
break;
}
}
}
}
}
public class Example11 {
public static void main(String[] args) {
Ticket1 ticket = new Ticket1();
new Thread(ticket, "线程一").start();
new Thread(ticket, "线程二").start();
new Thread(ticket, "线程三").start();
new Thread(ticket, "线程四").start();
}
}
运行结果中并没有出现线程二和线程三售票的语句,出现这样的现象是很正常的,因为线程在获得锁对象时有一定的随机性,在整个程序的运行期间,线程二和线程三始终未获得锁对象,所以未能显示它们的输出结果
同步代码块中的锁对象可以是任意类型的对象,但多个线程共享的锁对象必须是唯一的。“任意”说的是共享锁对象的类型。锁对象的创建代码不能放到run()方法中,否则每个线程运行到run()方法都会创建一个新对象,这样每个线程都会有一个不同的锁,每个锁都有自己的标志位,这样线程之间便不能产生同步的效果
6.5.3 同步方法
同步方法简介:同步代码块可以有效解决线程的安全问题,当把共享资源的操作放在synchronized定义的区域内时,便为这些操作加了同步锁。在方法前面同样可以使用synchronized关键字来修饰,被修饰的方法为同步方法,它能实现和同步代码块同样的功能
synchronized 返回值类型 方法名([参数1,…]){} 被synchronized修饰的方法在某一时刻只允许一个线程访问,访问该方法的其他线程都会发生阻塞,直到当前线程访问完毕后,其他线程才有机会执行该方法(电话亭效应)
实例:
class Ticket1 implements Runnable {
private int tickets = 10;
public void run() {
while (true) {
saleTicket();
if (tickets <= 0) {
break;
}
}
}
private synchronized void saleTicket() {
if (tickets > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "---卖出的票"
+ tickets--);
}
}
}
public class Example12 {
public static void main(String[] args) {
Ticket1 ticket = new Ticket1();
new Thread(ticket,"线程一").start();
new Thread(ticket,"线程二").start();
new Thread(ticket,"线程三").start();
new Thread(ticket,"线程四").start();
}
}
同步代码块的锁是自己定义的任意类型的对象,同步方法也有锁,它的锁就是当前调用该方法的对象,也就是this指向的对象。这样做的好处是,同步方法被所有线程所共享,方法所在的对象相对于所有线程来说是唯一的,从而保证了锁的唯一性。当一个线程执行该方法时,其他的线程就不能进入该方法中,直到这个线程执行完该方法为止。从而达到了线程同步的效果
注意:Java中静态方法的锁是该方法所在类的class对象,该对象在装载该类时自动创建,该对象可以直接用类名.class的方式获取
同步代码块和同步方法的弊端:同步解决了多个线程同时访问共享数据时的线程安全问题,只要加上同一个锁,在同一时间内只能有一条线程执行。但是线程在执行同步代码时每次都会判断锁的状态,非常消耗资源,效率较低
6.5.4 死锁问题
死锁问题简介:两个线程在运行时都在等待对方的锁,这样便造成了程序的停滞,这种现象称为死锁
实例:
package Eight;
public class test07 {
public static class DeadLock implements Runnable{
static Object chopsticks = new Object();
static Object knifeandfork = new Object();
private boolean flag;
DeadLock(boolean flag){
this.flag=flag;
}
@Override
public void run() {
if(flag) {
while(true) {
synchronized(chopsticks) {
System.out.println(Thread.currentThread().getName()+"---if---chopsticks");
synchronized(knifeandfork) {
System.out.println(Thread.currentThread().getName()+ "---if---knifeAndFork");
}
}
}
}else {
while(true) {
synchronized(knifeandfork) {
System.out.println(Thread.currentThread().getName()+"---if---knifeandfork");
synchronized(chopsticks) {
System.out.println(Thread.currentThread().getName()+ "---if---chopsticks");
}
}
}
}
}
}
public static void main(String[] args) {
DeadLock d1 = new DeadLock(true);
DeadLock d2 = new DeadLock(false);
new Thread(d1, "Chinese").start();
new Thread(d2, "American").start();
}
}
如果文章对您有所帮助,记得一键三连支持一下哦~
|