APIAPI

第6章 Java高级应用
在进行Java编程的过程中,我们为了处理各种复杂的应用场景,需要使用到不同领域的处理逻辑,Java将这些处理逻辑都封装成为了API,并按照不同的应用角度进行了区分,形成不同的Java包。这些Java包构造了我们变成的过程中所需要的高级应用。
6.1 异常处理
在进行代码编写的过程中,根据处理的不同问题,会存在不同的异常情况,针对这些异常情况,我们需要针对性的处理,Java提供了完成的异常处理机制以方便我们在开发中更好的处理各种异常情况。
6.1.1 异常简介
Java代码中根据需要处理的问题的严重程度,划分为错误还有异常,对于错误来说一般是由于代码导致的,这个时候需要针对不同的业务逻辑进行代码的更改以及调整。对于异常来说主要划分为两种情况的异常,主要分为编译时期异常还有运行时期异常。对于Java代码来说,我们主要分为两个阶段,分别为Java代码的编译阶段、Java代码的执行阶段,对于在编译阶段出现的异常,我们称为编译时期异常,对于运行阶段的出现的异常我们称为运行时期异常。针对这两种异常我们我们需要使用不同的处理方案。
在Java的API体系中有一个接口,他就是java.lang.Throwable,这个接口是Java中所有异常的顶层接口,其下分为两个分支分别为Error(错误)、Exception(异常),对于Error来说主要由于我们编写代码的错误导致,Exception是我们平时说的异常,Java针对异常提供了完成的处理方案。
在进行Java编译时,出现的异常称为编译时期异常,常见的编译时期异常有:ClassNotFoundException:称为java类找不到的异常,这一般情况下是由于引用类错误的Java类,可能是名字写错,或者包路径写错,导致找不到对应的Java类;DataFormatException:称为数据格式化异常,一般发生在将某些数据按照一定的格式进行转换时出现的异常,这个时候需要确定好原始数据与期望转换为的格式的对应关系;IOException:称为I/O异常,对于文件系统的操作,主要包括数据的输入与数据的输出操作,在这个过程中出现的异常被称为I/O异常,此外I/O异常是一个很大的分类,其下属划分多种不同的I/O异常情况,比如:CharacterCodingException,当出现字符编码或解码错误时,抛出此经过检查的异常,ClosedChannelException,当试图对已关闭的信道进行调用完成操作时,抛出此异常,EOFException,当I/O流意外到达了结尾的时候抛出该异常,FileNotFoundException,称为文件找不到的异常,当进行I/O操作时,如果指定类一个不存在的文件路径,那么就会出现文件找不到的异常,UnknownHostExcetion,称为未知主机异常,当进行网络I/O操作时,如果指定到了一个不存在的IP地址,那么对应这个IP地址在对应的网络中没有主机预制对应,则会出现未知主机异常;TimeoutException,称为超时异常,对于需要制定超时时间的阻塞操作来说,当阻塞的时间超过限制就会出现该异常。
对于上面的这些异常都是编译时期可能会出现的异常情况,Java官方将他们封装成为了不同的Java类,用来进行区分,便于管理与记忆。
除了编译时期的异常之外,Java还存在运行时期异常,这些异常主要发生在Java代码的运行过程中,主要是由于代码中存在一些逻辑错误导致的,在Java的异常体系中有一个运行时期异常的顶层父类那就是RuntimeException,称为运行时期异常,在其子类中存在各种不同原因的运行时期异常,常见的运行时期异常有:ArrayStoreException,数组存储的异常,当错误的数据类型的值存储到数组中时,出现的异常;ClassCastExcetion,称为类型转换异常,当试图将一个引用数据类型的实例转换为除了他的子父类之外的其他类型时,会出现类型转换异常;ConcurrentModificationException,称为并行修改异常,当对于结合使用迭代器的同时,进行数据的删除操作,会导致该异常;IndexOutOfBoundsException,称为索引越界异常,当对具有索引的集合或者数组进行操作时,其索引的值超出允许的范围时,出现该异常;NullPointerException,称为空指针异常,当引用数据类型的对象为null时,如果使用该对象去调用方法,会出现空指针异常;UnknownTypeException,未知类型的异常,当遇到未知种类的数据类型时,出现的异常。
除了官方定义的异常之外,我们在编写程序的过程中,我们也可以定义自己的异常类,用来标识不同的异常类型,这个时候的异常类需要继承官方的RuntimException,作为运行时异常的一种,在进行编程过程中,根据不同的项目需求,可以根据具体情况来制定一套符合项目本身情况的异常体系。具体的异常定义格式为,定义一个Java类继承自RuntimeException,编写有参构造方法,在构造方法中通过super调用父类的有参构造方法,具体的代码如下:
public class MyException extends RuntimeException{

/**
 * 自定义异常的有参构造方法
 * @param msg 改变了用来记录异常的信息
 */
public MyException(String msg)
{
	//调用父类的构造方法,完成异常信息的初始化
	super(msg);
}

}
定义了异常处理的类之后,在代码的逻辑中如果遇到需要处理的异常情况,可以通过创建MyException的实例来完成。

6.1.2 异常处理机制
对于Java的异常体系来说,存在编译时期异常以及运行时期异常,这两种异常情况分别使用不同的处理方式来完成。对于编译时期异常,存在两种梳理方案,第一种处理方案就是直接将异常的信息直接抛出,不进行人为处理,将异常信息传递给JVM虚拟机,虚拟机会将异常信息输出到屏幕上,第二种处理方式就是使用try…catch来进行异常的捕获操作,根据不同的异常情况,可以人为去进行处理。对于运行时期异常,一般是由于代码逻辑不够严谨导致的问题,这个时候,需要加入合适的条件判断,让代码的逻辑更加严谨,从而符合编程的整体逻辑,在严重的时候可能需要从头开始考虑代码的具体实现。
我们通过代码,展示一下针对编译时期异常的处理方式,第一种将异常进行抛出,代码如下:
public class Test {

//程序的入口
public static void main(String[] args) throws IOException
{
	//源文件路径
	String fromPath = "C:\\AAAA\\a.txt";
	//目标文件路径
	String toPath = "C:\\BBBBBB";
	
	doCopy(fromPath, toPath);
	
}

//自定义方法完成文件的复制操作
public static void doCopy( String fromPath, String toPath ) throws IOException
{
	//对于传入的参数,需要进行初步判断,防止错误情况的发生
	if( null == fromPath || null == toPath )
	{
		System.out.println( "源路径与目标路径均不能为空" );
		return;
	}
	
	//检查两个路径对应的文件是否都存在,尤其是源文件路径
	File fromFile = new File( fromPath );
	
	if( !fromFile.exists() && !fromFile.isFile() )
	{
		System.out.println( "源文件不存在或者不是文件" );
		return;
	}
	
	//对于目标路径来说,应该是一个文件夹,也就是一个目录,并且应该存在
	File toFile = new File( toPath );
	
	if( !toFile.exists() && !toFile.isDirectory() )
	{
		System.out.println( "目标目录不存在或者不是目录" );
		return;
	}
	
	//这行代码会出现文件找不到的异常 FileNotFoundException
	FileInputStream fis = new FileInputStream( fromFile );
	
	int len = 0;
	byte[] buff = new byte[1024];
	
	//获取源文件的名称
	String fileName = fromFile.getName();
	//将目标路径与文件名进行拼接
	String toAbsPath = toPath +"\\"+ fileName;
	
	//这行代码会出现文件找不到的异常 FileNotFoundException
	FileOutputStream fos = new FileOutputStream(toAbsPath);
	
	//read方法会出现 IOException
	while((len = fis.read(buff))!=-1)
	{
		//write方法会出现 IOException
		fos.write(buff, 0, len);
		fos.flush();
	}
	
	//close方法会出现 IOException
	fos.close();
	fis.close();
}

}
在上面的代码中,可以看到多个代码位置都存在编译时异常,使用抛出的方式进行了处理,主要就是在方法的声明上使用throws关键字,将异常的类型进行抛出,并且当发生方法调用操作时,这个抛出时需要继续的,直到抛出到main方法的方法声明上。
除了将编译时期的异常进行抛出处理之外,我们还可以使用try、catch进行处理,try表示尝试代码,可以将存在异常的代码放在try后的大括号内,catch进行异常的捕获操作,并根据不同的异常信息分为不同的捕获,具体的代码处理可以是如下面代码那样:
public class Test {

//程序的入口
public static void main(String[] args)
{
	//源文件路径
	String fromPath = "C:\\AAAA\\a.txt";
	//目标文件路径
	String toPath = "C:\\BBBBBB";
	
	doCopy(fromPath, toPath);
	
}

//自定义方法完成文件的复制操作
public static void doCopy( String fromPath, String toPath ) 
{
	//对于传入的参数,需要进行初步判断,防止错误情况的发生
	if( null == fromPath || null == toPath )
	{
		System.out.println( "源路径与目标路径均不能为空" );
		return;
	}
	
	//检查两个路径对应的文件是否都存在,尤其是源文件路径
	File fromFile = new File( fromPath );
	
	if( !fromFile.exists() && !fromFile.isFile() )
	{
		System.out.println( "源文件不存在或者不是文件" );
		return;
	}
	
	//对于目标路径来说,应该是一个文件夹,也就是一个目录,并且应该存在
	File toFile = new File( toPath );
	
	if( !toFile.exists() && !toFile.isDirectory() )
	{
		System.out.println( "目标目录不存在或者不是目录" );
		return;
	}
	
	//将局部变量的作用范围向上提高一层
	FileInputStream fis = null;
	FileOutputStream fos = null;
	
	try{
	
		//这行代码会出现文件找不到的异常 FileNotFoundException
		fis = new FileInputStream( fromFile );
		
		int len = 0;
		byte[] buff = new byte[1024];
		
		//获取源文件的名称
		String fileName = fromFile.getName();
		//将目标路径与文件名进行拼接
		String toAbsPath = toPath +"\\"+ fileName;
		
		//这行代码会出现文件找不到的异常 FileNotFoundException
		fos = new FileOutputStream(toAbsPath);
		
		//read方法会出现 IOException
		while((len = fis.read(buff))!=-1)
		{
			//write方法会出现 IOException
			fos.write(buff, 0, len);
			fos.flush();
		}
	}catch (FileNotFoundException e) {
		e.printStackTrace();
		System.out.println( "这里可以书写针对特定异常的处理逻辑" );
	} catch (IOException e) {
		e.printStackTrace();
		System.out.println( "这里可以书写针对特定异常的处理逻辑" );
	} finally{
		//close方法会出现 IOException
		try {
			//当fos、fis不是null时,再调用close方法,否则会出现NullPointerException
			if(null != fos){
				fos.close();
			}
			if(null != fis){
				fis.close();
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		
	}
}

}
上面的代码对于出现的编译时期异常进行了try…catch…finally的处理,捕获了代码中可能存在的异常信息,并且可以根据不同的处理逻辑来安排各自的异常信息处理。
针对于编程过程中的一些环节,我们可以通过自定义的异常体系,来完成异常的管理和处理,这样可以更加灵活的处理代码中的不同问题,比如对于上面的代码中,对于形式参数的一些基础判断信息,如果仅仅是向屏幕输出,并不是很合适,这里可以通过自定义的异常来完成这个处理,更为合理,代码如下:
public class MyException extends RuntimeException{

/**
 * 自定义异常的有参构造方法
 * @param msg 改变了用来记录异常的信息
 */
public MyException(String msg)
{
	//调用父类的构造方法,完成异常信息的初始化
	super(msg);
}

}

public class Test {

//程序的入口
public static void main(String[] args)
{
	//源文件路径
	String fromPath = null;
	//目标文件路径
	String toPath = "C:\\BBBBBB";
	
	try{
		doCopy(fromPath, toPath);
	}catch(MyException e)
	{
		e.printStackTrace();
	}
}

//自定义方法完成文件的复制操作
public static void doCopy( String fromPath, String toPath )
{
	//对于传入的参数,需要进行初步判断,防止错误情况的发生
	if( null == fromPath || null == toPath )
	{
		//System.out.println( "源路径与目标路径均不能为空" );
		throw new MyException( "源路径与目标路径均不能为空" );
		
	}
	
	//检查两个路径对应的文件是否都存在,尤其是源文件路径
	File fromFile = new File( fromPath );
	
	if( !fromFile.exists() && !fromFile.isFile() )
	{
		//System.out.println( "源文件不存在或者不是文件" );
		throw new MyException( "源文件不存在或者不是文件" );
		
	}
	
	//对于目标路径来说,应该是一个文件夹,也就是一个目录,并且应该存在
	File toFile = new File( toPath );
	
	if( !toFile.exists() && !toFile.isDirectory() )
	{
		//System.out.println( "目标目录不存在或者不是目录" );
		throw new MyException( "目标目录不存在或者不是目录" );
	}
	
	//将局部变量的作用范围向上提高一层
	FileInputStream fis = null;
	FileOutputStream fos = null;
	
	try{
	
		//这行代码会出现文件找不到的异常 FileNotFoundException
		fis = new FileInputStream( fromFile );
		
		int len = 0;
		byte[] buff = new byte[1024];
		
		//获取源文件的名称
		String fileName = fromFile.getName();
		//将目标路径与文件名进行拼接
		String toAbsPath = toPath +"\\"+ fileName;
		
		//这行代码会出现文件找不到的异常 FileNotFoundException
		fos = new FileOutputStream(toAbsPath);
		
		//read方法会出现 IOException
		while((len = fis.read(buff))!=-1)
		{
			//write方法会出现 IOException
			fos.write(buff, 0, len);
			fos.flush();
		}
	}catch (FileNotFoundException e) {
		e.printStackTrace();
		System.out.println( "这里可以书写针对特定异常的处理逻辑" );
	} catch (IOException e) {
		e.printStackTrace();
		System.out.println( "这里可以书写针对特定异常的处理逻辑" );
	} finally{
		//close方法会出现 IOException
		try {
			//当fos、fis不是null时,再调用close方法,否则会出现NullPointerException
			if(null != fos){
				fos.close();
			}
			if(null != fis){
				fis.close();
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		
	}
}

}
通过自定义异常的方式,可以将代码中存在的各种复杂情况通过自定义的异常类,来进行统一管理,从而使得项目更加严谨,处理方式更加统一,方便开发。
除了对编译时期的异常进行处理外面我们还需要对运行时期的异常进行处理,其处理方式主要为调整编程的逻辑结构使得程序更加严谨,比如上面的代码中对于输入流对象fis、输出流对象fos的关闭操作来说,当上面的代码出现异常信息时,极容易会导致这两个对象为null,这个时候,就需要在代码中加入合理的判断操作,从而避免异常的出现,否则会出现NullPointerException空指针异常。除此之外当我们进行数学运算操作时,对于除法操作我们知道,除数时不能为0的,当除数为0时,运行程序就会出现ArithmeticException异常,表示除数不能为0,这个时候,是由于代码逻辑的错误导致,需要更改代码的逻辑,还有在进行数组操作时,由于不注意经常会出现ArrayOutOfBoundsException,称为数组越界异常,这是由于在使用数组时,下表超出了数组的索引边界导致,这个时候就需要我们对数组的索引进行操作时,检查好其边界情况。

6.2 工具类
编程时用来解决工作与生活的实际问题的,在编程过程中我们需要将具体问题的处理过程中的各个环节对应到代码中,无论使用Java语言来编程还是谁用其他语言来编程,都离不开一些具体的工具,这些工具主要包括数据的表示、网络操作、读写操作、线程操作等多个方面。Java提供了完善的编程工具来让编程人员直接应用。
6.2.1 Java API
API(Application Platform Interface)应用平台编程接口,这是Java为我们更好的使用Java虚拟机提供的各种编程接口而汇总出来的编程接口,所谓的编程接口指的就是当我们需要使用某项功能时,不需要自己进行从无到有的开发,而是直接根据已有的工具直接进行使用,就如同前面的示例中使用到的键盘录入工具,当我们需要通过键盘来录入一些信息时,不需要考虑从硬件到驱动再到操作系统等如何去实现,我们直接借助Scanner这样的一个Java类,通过创建其实例,并调用相关的方法,直接就得到了不同的录入信息,根据不同的方法作用我们可以录入整数、小数、字符串等多种信息。Scanner就是JVM对外提供出来的一个编程的接口,编程人员通过使用这个接口,就完成了相应的编程操作。
在整个Java编程过程中,我们需要使用很多编程的接口也就是API,才能完成我们需要处理的业务逻辑。我们比较常用的API主要包含以下几个领域:Java的集合体系相关主要在java.util包中,Java的I/O体系,主要进行文件的输入输出操作,在java.io包中,Java的网络编程操作,主要在java.net包中,Java的多线程体系,用来完成对于线程的创建以及管理等操作,主要在java.lang包中。对于不同的包都存在各自的API体系,来组织其中的API,下面我们一次介绍一下,常用的API以及他们的实际应用实例。
Java的I/O包,java.io这个包中主要包括进行输入输出操作相关的Java类,其中类File是对应于文件系统的文件或者文件夹,该类提供了多个与文件操作相关的功能,主要如下:
构造方法:
File(File parent, String child) 通过文件的父目录对应的文件对象以及文件名称创建File的对象
File(String pathName) 通过指向文件的路径名进行File对象的创建操作
成员方法:
boolean delete() 删除文件对象对应路径的文件或者目录
boolean exits() 判断文件对象对应路径的文件或者目录是否存在
String getAbslotePath() 获取文件对象对应的文件的绝对路径
String getName() 获取文件对象对应文件或者目录的名称
boolean isFile() 判断文件对象对应的是否为一个文件
boolean isDirectory() 判断文件对象对应的是否为一个目录
long lastModified() 获取文件对象对应的文件的最后修改时间
long length() 获取文件对象对应的文件的大小,文件长度
File[] listFiles() 获取文件对象对应的目录下的所有文件的文件对象
当然这些方法只是很少的一部分,还有很多功能在Java的官方文档中,对于API我们需要将其应用到具体的编程实践中,下面我们使用上面的API完成,一个对于某个目录的递归操作,并在递归过程中,打印出文件的一些基本信息,代码如下:
public class Test {

//程序入口
public static void main(String[] args)
{
	String dirPath = "C:\\AAAA";
	
	printMsg(dirPath);
}

//自定义方法完成打印目录下文件的详细信息
public static void printMsg(String dirPath)
{
	//对于传递进来的参数进行判断
	if(null == dirPath)
	{
		System.out.println("路径不能为空");
		return;
	}
	
	//通过File类创建文件对象
	File dirFile = new File(dirPath);
	
	//判断该文件对象对应的是否为一个普通文件
	if(dirFile.isFile())
	{
		System.out.println( "文件名称:"+dirFile.getName() );
		System.out.println( "文件大小:"+dirFile.length() );
		System.out.println( "文件绝对路径:"+dirFile.getAbsolutePath() );
	}
	
	//判断该文件对象对应的是否为一个目录
	if(dirFile.isDirectory())
	{
		//当文件对象是目录时,获取该目录下的所有子文件的文件信息
		File[] files = dirFile.listFiles();
		
		//通过增强for遍历上面的数组
		for(File file : files)
		{
			//如果遍历到的文件是一个普通文件
			if(file.isFile())
			{
				System.out.println( "文件名称:"+dirFile.getName() );
				System.out.println( "文件大小:"+dirFile.length() );
				System.out.println( "文件绝对路径:"+dirFile.getAbsolutePath() );
			}
			
			//如果遍历到的文件是一个目录
			if(file.isDirectory())
			{
				System.out.println( "目录名称:"+file.getName() );
				//获取文件对象对应的绝对路径
				String absPath = file.getAbsolutePath();
				
				//递归调用方法自己完成操作
				printMsg(absPath);
				
			}
		}
	}
}

}
代码的运行结果如图:

Java的I/O包中,还有关于文件流处理的相关API,分别为字节输入流FileInputStream,字节输出流FileOutputStream,字符输入流FileReader,字符输出流FileWriter,通过这些流可以完成文件的内容复制操作,在字节流的类中我们常用的方法如下:

FileInputStream类
构造方法
FileInputStream(File file) 通过文件对象创建指向该文件的输入流
FileInputStream(String pathName) 通过文件路径创建指向该文件的输入流
成员方法
int read(byte[] buff) 将文件中的内容读取到字节数组之中
void close() 关闭数据流的方法
FileOutputStream类
构造方法
FileOutputStream(File file) 通过文件对象创建指向该文件的输出流
FileOutputStream(String pathName) 通过文件路径创建指向该文件的输出流
成员方法
void write(byte[] buff , int offset, int len) 将字节数组中的指定长读的字节数据写入到文件中
void flush() 将输出流中的数据刷新出去
void close() 关闭数据流的方法
我们通过字节的输入与输出操作,完成一个文件的赋值,代码如下:
public class Test {

//程序的入口
public static void main(String[] args) throws IOException
{
	//源文件路径
	String fromPath = "C:\\AAAA\\a.txt";
	//目标文件路径
	String toPath = "C:\\BBBBBB";
	
	doCopy(fromPath, toPath);
	
}

//自定义方法完成文件的复制操作
public static void doCopy( String fromPath, String toPath ) throws IOException
{
	//对于传入的参数,需要进行初步判断,防止错误情况的发生
	if( null == fromPath || null == toPath )
	{
		System.out.println( "源路径与目标路径均不能为空" );
		return;
	}
	
	//检查两个路径对应的文件是否都存在,尤其是源文件路径
	File fromFile = new File( fromPath );
	
	if( !fromFile.exists() || !fromFile.isFile() )
	{
		System.out.println( "源文件不存在或者不是文件" );
		return;
	}
	
	//对于目标路径来说,应该是一个文件夹,也就是一个目录,并且应该存在
	File toFile = new File( toPath );
	
	if( !toFile.exists() || !toFile.isDirectory() )
	{
		System.out.println( "目标目录不存在或者不是目录" );
		return;
	}
	
	//创建文件的输入流
	FileInputStream fis = new FileInputStream( fromFile );
	
	int len = 0;
	byte[] buff = new byte[1024];
	
	//获取源文件的名称
	String fileName = fromFile.getName();
	//将目标路径与文件名进行拼接
	String toAbsPath = toPath +"\\"+ fileName;
	
	//创建文件的输出流
	FileOutputStream fos = new FileOutputStream(toAbsPath);
	
	//循环从源文件中读取数据
	while((len = fis.read(buff))!=-1)
	{
		//将读取到的数据写入到文件中
		fos.write(buff, 0, len);
		fos.flush();
	}
	
	//关闭输入与输出流,先打开的后关闭
	fos.close();
	fis.close();
}

}
对于字符流的相关API主要包含下面的这些内容:
FileReader类
构造方法
FileReader (File file) 通过文件对象创建指向该文件的输入流
FileReader (String pathName) 通过文件路径创建指向该文件的输入流
成员方法
int read(char[] buff) 将文件中的内容读取到字符数组之中
void close() 关闭数据流的方法
FileWriter类
构造方法
FileWriter (File file) 通过文件对象创建指向该文件的输出流
FileWriter (String pathName) 通过文件路径创建指向该文件的输出流
成员方法
void write(char[] buff , int offset, int len) 将字节数组中的指定长读的字符数据写入到文件中
void flush() 将输出流中的数据刷新出去
void close() 关闭数据流的方法
我们同样适用字符相关的操作流,完成文件的复制操作,具体代码如下:
public class Demo {

//程序的入口
public static void main(String[] args) throws IOException
{
	//源文件路径
	String fromPath = "C:\\AAAA\\a.txt";
	//目标文件路径
	String toPath = "C:\\BBBBBB";
	
	doCopy(fromPath, toPath);
	
}

//自定义方法完成文件的复制操作
public static void doCopy( String fromPath, String toPath ) throws IOException
{
	//对于传入的参数,需要进行初步判断,防止错误情况的发生
	if( null == fromPath || null == toPath )
	{
		System.out.println( "源路径与目标路径均不能为空" );
		return;
	}
	
	//检查两个路径对应的文件是否都存在,尤其是源文件路径
	File fromFile = new File( fromPath );
	
	if( !fromFile.exists() && !fromFile.isFile() )
	{
		System.out.println( "源文件不存在或者不是文件" );
		return;
	}
	
	//对于目标路径来说,应该是一个文件夹,也就是一个目录,并且应该存在
	File toFile = new File( toPath );
	
	if( !toFile.exists() && !toFile.isDirectory() )
	{
		System.out.println( "目标目录不存在或者不是目录" );
		return;
	}
	
	//创建文件的输入流
	FileReader fr = new FileReader( fromFile );
	
	int len = 0;
	char[] buff = new char[1024];
	
	//获取源文件的名称
	String fileName = fromFile.getName();
	//将目标路径与文件名进行拼接
	String toAbsPath = toPath +"\\"+ fileName;
	
	//创建文件的输出流
	FileWriter fw = new FileWriter(toAbsPath);
	
	//循环从源文件中读取数据
	while((len = fr.read(buff))!=-1)
	{
		//将读取到的数据写入到文件中
		fw.write(buff, 0, len);
		fw.flush();
	}
	
	//关闭输入与输出流,先打开的后关闭
	fw.close();
	fr.close();
}

}
对于编程中关于文件复制的需求,我们可以使用关于I/O的相关API来完成操作。
在我们实际项目编程过程中时离不开网络操作的,尤其是移动互联网的发展,使得网络称为沟通大家生活方方面面的重要一个环节,这里介绍一下,关于网络操作的相关API,基于OSI七层参考模型的传输层的TCP协议,我们可以完成网络的数据传输操作,具体的API如下:
Socket类
构造方法
Socket(String ipAddress, int port) 通过服务器端的IP地址与端口号,创建网络客户端
成员方法
InputStream getInputStream() 通过网络连接获取输入流
OutputSteam getOutputStream() 通过网络连接获取输出流

ServerSocket类
构造方法
ServerSocket(int port) 通过制定服务端程序的工作端口号,创建服务端对象
成员方法
Socket accept() 负责监听来自客户端的访问操作,并接受访问连接
我们可以通过上面的API来完成这样的一个需求,从客户端发送一个信息到服务端,服务端接收到信息之后,返回一个信息给客户端,从而完成一次网络的信息交互,代码如下:
客户端代码:
public class MyClient {

//程序入口
public static void main(String[] args) throws UnknownHostException, IOException
{
	String ipAddress = "127.0.0.1";
	int port = 9999;
	
	sendData(ipAddress, port);
	
}

//完成客户端的网络操作
public static void sendData(String ipAddress, int port) throws UnknownHostException, IOException
{
	
	//通过IP以及端口号创建客户端
	Socket sk = new Socket(ipAddress,port);
	
	//基于网络的连接打开输出流,用来向服务端发送数据
	OutputStream os = sk.getOutputStream();
	
	//通过网络输出流构造字符处理的输出流
	DataOutputStream dos = new DataOutputStream(os);
	
	//向服务端发送一个字符串信息
	dos.writeUTF("Hello");
	
	//进行数据刷新
	dos.flush();
	
	//数据发送后,等待服务端的信息回复
	InputStream is  = sk.getInputStream();
	
	//基于网络的输入流,创建字符处理的输入流
	DataInputStream dis = new DataInputStream(is);
	
	//从输入流中读取回复的信息
	String recMsg = dis.readUTF();
	
	System.out.println( "收到服务端的信息回复为:"+recMsg );
	
	//当服务端回复的信息为OK时,关闭这一次网络交互
	if( "OK".equals(recMsg) )
	{
		sk.close();
	}
}

}
服务端代码:
public class MyServer {

//程序入口
public static void main(String[] args) throws IOException
{
	int port = 9999;
	
	recData(port);
}

//自定义方法完成服务端接收数据的操作
public static void recData(int port) throws IOException
{
	//创建服务端并指定工作的端口号
	ServerSocket ss = new ServerSocket(port);
	
	//调用服务端的监听方法,用来接收来自客户端的访问
	Socket sk = ss.accept();
	
	//打开网络的输入流,用来接收输入的输入
	InputStream is = sk.getInputStream();
	
	//通过网络的输入流,创建字符操作的输入流
	DataInputStream dis = new DataInputStream(is);
	
	//读取从客户端发送的数据信息
	String recMsg = dis.readUTF();
	
	System.out.println( "服务端收到客户端的信息:"+recMsg );
	
	//收到信息后准备给客户端进行回复操作
	OutputStream os = sk.getOutputStream();
	
	//基于网络的输出流,创建字符处理的输出流
	DataOutputStream dos = new DataOutputStream(os);
	
	//当收到客户端发送的Hello信息时,回复一个OK
	if("Hello".equals(recMsg))
	{
		dos.writeUTF("OK");
		dos.flush();
	}
	
}

}
通过上面的代码我们看到了,通过java.net相关的API完成了,网络中的数据交互操作。当然在实际的编程过程中我们会遇到更多复杂逻辑的处理以及API使用,通过API的基本介绍希望大家掌握API的基本学习方式,并尝试使用API完成具体的业务处理为目标,通过大量的代码实验来学习。
6.2.2 Object
在我们进行Java编程的过程中,我们将具有相同属性特征以及行为功能的个体,进行抽象和概括,从而得到一个Java类,使用成员变量来表示属性信息,使用成员方法来表示行为功能。Java的API就是将我们在开发过程中需要经常使用到的功能封装成为了一个个Java类,我们借助于这些Java类进一步使用诸如:网络编程、输入输出操作、多线程编程、集合等多个方面的技术。无论是官方提供好的API中的Java类还是我们为了解决编程问题,自己定义的Java类,这些类都有一个统一的父类,那就是Object,表示万事万物的意思,在Java中任何一个Java类都直接或者间接的继承了Object,之所以这么做的原因是通过Object为所有的子类统一规划相关的功能,因为在我们使用Java类中有一些操作时固定的且必须的,比如:将一个Java类的对象的所有成员信息输出、判断两个Java类的对象是否相等、生成哈希值等等。
在Object类中规划了一些常用功能,为其子类进行了统一的规划,这些常用的功能如下:
boolean equals(Object obj)
判断相等的方法,equals用作比较调用该方法的对象与形式参数中的对象是否相当的判断,该功能的的具体实现是将两个对象中的所有成员变量依次对比,只有在所有成员变量的值都相同时,才会返回true,否则返回false。
int hashCode()
获取哈希值的功能方法,哈希值指的是对于引用类型的数据获取的特征值,可以通过计算成员变量的数量,对象的内存地址,某个成员变量长度,然后将这些值进行算术运算得到的一个特征值,这个特征值可以唯一标识这个数据,这样的一个过程称为哈希运算,得到的值为哈希值。
对于hashCode方法与equals方法,这两者经常被一起使用,hashCode方法用来为某个Java类生成一个唯一的哈希值,使得所有通过该Java类创建出来的对象都具有相同的哈希值,equals用来比较两个对象的成员变量信息判断其是否相同。这两个方法配合在需要去除元素重复的场景中,先通过判断哈希值是否相同,在相同的前提下,再去调用equals方法,这样可以节省操作,提高代码效率。
protected Object clone()
进行Java类的对象的克隆操作,对于引用数据类型来说,当我们出现如下操作时,需要注意:Student stu1 = new Student(); Student stu2 = stu1; 这个代码中只有一个Student类的实例存在,Student stu2 = stu1; 这个操作只是进行了引用的赋值操作,并没有创建新的对象,这个时候如果对stu2中的成员变量进行修改时,就会发现,stu1中的数据也跟着变化了,这是因为两个引用指向了同一个对象,这种操作在编程过程中会引起错误,如果想让stu2与stu1拥有相同的数据,但是又相互不干扰,那么可以使用上面的clone方法来完成,这个时候会重新创建一个新的实例,然后将引用赋值给stu2。
Class<?> getClass()
获取某个对象的类,也就是根据某个类的实例对象获取其对应的Java类。
protected void finalize()
对于Java语言来说,其不具有直接操作内存的能力,不会像C、C++这样的语言,直接可以开辟内存区域以及释放内存,这些操作,由JVM虚拟机帮助我们实现了,所以对于Java的编程过程中,我们很好直接处理内存。该方法就是用于内存回收时,调用的方法。当垃圾回收器确定运行的程序中不存在对于该对象的引用时,就会调用该方法,完成垃圾回收操作。
String toString()
该方法的作用是将类的对象转换为字符串,并返回。在进行Java编程的过程中,我们经常需要将Java类的对象打印出来,以观察其数据信息,该方法就为我们提供很好的帮助,在我们定义不同的Java类时,都要重写该方法,以方便我们查看该类对应的实例的数据信息。
void wait()
从方法名称上我们可以看到起是等待的意思。在我们进行多线程编程操作的时候,如果多个线程之间的运行时彼此存在依赖关系的,也就睡线程一的执行需要线程二的某种支持,那么当线程一先执行的时候,遇到满足不了的条件时,可以通过线程中的锁对象,调用该方法来完成等待,直到另外一个线程执行,使得为满足的条件满足,当前线程再执行。
void notify()
在我们多个线程执行的过程中,就如上面描述中某个线程需要另外一个线程提供的条件才能继续执行,那么当另外一个线程执行后,可以调用notify方法通知正在等待的线程,使其继续之前的逻辑,从而完成后续操作。
总的来说,对于Object类作为Java的API体系中所有类的父类,它为所有的Java类统一规划了需要完成的功能。我们需要根据不同的情况来对这些方法进行重写或者直接继承过来使用。
6.2.3 包装类
在Java中,针对于数据类型我们可以分为两大类,基本数据类型与引用数据类型,对于基本数据类型来说主要用来操作一些基础性的简单数据,比如说数字,单个字符等等。对于引用数据类型来说具有更多层面的信息,比如表示属性特征的成员变量,表示行为功能的成员方法,内容更加丰富。同样引用数据类型的使用更加符合面向对象的编程思想,我们可以通过类的实例来使用类的属性以及功能,但对于基本数据类型来说,他们不具有这样的特点。只能是进行简单的数据操作,不能像引用数据类型那样调用属性与功能。为了解决这个问题,在Java中提供了对于基本数据类型进行封装的Java类,这些类经常被我们称为封装类,这些封装类中提供了相关的属性与功能。
对于基本数据类型int,在Java的API中提供了一个Java类Integer,作为其的封装类。Integer类为我们提供了一系列常用功能。
字段信息:
static int MAX_VALUE 值为2^31-1的常量,表示int类型能够表示的最大值
static int MIN_VALUE 值为 -2^31的常量,表示int类型能够表示的最小值
构造方法:
Integer(int value) 创建一个Integer的对象,其值为形式参数的值
Integer(String str) 创建一个Integer的对象,其值为str表示的数值,注意,此时str的内容必须是数字的字符串形式,不可以是任意字符串

成员方法:
int intValue() 以int类型返回Integer对象代表的值
long longValue() 以long类型返回该Integer对象的值
double doubleValue() 以double类型返回该Integer对象的值
static int parseInt(String str) 将字符串str所代表的的整数解析出来,获取其中的数值
String toString() 将Integer对象转换为String字符串
我们使用代码演示Integer相关的API的使用,代码如下:
public class IntegerTest {

public static void main(String[] args)
{
	//通过Integer的构造方法创建对象
	Integer num1 = new Integer(1);
	System.out.println( "数值为:"+num1 );
	
	Integer num2 = new Integer("2");
	System.out.println( "数值为:"+num2 );

	//int可表示的最大值
	System.out.println( "最大值:"+Integer.MAX_VALUE );
	
	//int克表示的最小值
	System.out.println( "最小值:"+Integer.MIN_VALUE );
	
	//从Integer对象中获取基本数据类型的值
	int num3 = num1.intValue();
	System.out.println( "基本类型的数值为:"+num3 );
	
	//对字符串进行解析,得到数字
	int num4 = Integer.parseInt("5");
	System.out.println( "解析后的值为:"+num4 );
	
}

}
代码的运行结果如图:

在上面的代码中我们可以看到对于基本数据类型int来说,将其封装到引用数据类型Integer中,使其具有了诸多快捷的功能操作。
对于上面代码中,我们通过构造方法,将一个基本数据类型的值封装到一个Integer对象中,这样的一个过程称为手动装箱操作,简单的来说就是将基本数据类型int的某个值,装到了引用数据类型Integer的对象中。通过intValue方法我们可以将封装在Integer对象中的基本数据类型的值手动获取出来,这样的一个过程我们称为手动拆箱操作。
对于基本数据类型与其对应的引用数据类型之间的装箱以及拆箱操作,除了手动完成之外,是可以支持到自动完成的,不需要特殊的操作,就可以很方便的进行转换。我们看一下代码的实现。代码如下:
public class IntegerTest {

public static void main(String[] args)
{
	
	//定义一个整数类型变量
	int num1 = 20;
	
	//自动装箱操作
	Integer num2 = num1;
	
	System.out.println( "数值为:"+num2 );
	
	//自动拆箱操作
	int num3 = num2;
	
	System.out.println( "数值为:"+num3 );
	
}

}
代码的运行结果如图:

对于基本数据类型对应的引用数据类型来说,进行装箱以及拆箱操作,可以像上面的代码那样,直接来完成,这个时候Java的API就会自动完成装箱以及拆箱操作,不需要程序员去进行手动操作。
对于所有的基本数据类型,在Java的API中,都提供了对应的封装类型。基本数据类型包括,整数类型:byte、short、int、long 浮点数类型:float、double 布尔类型:boolean字符类型:char。对于的包装类型分别为:Byte、Short、Integer、Long、Float、Double、Boolean、Character。对于Java中的包装类型,都具有装箱与拆箱功能,具体的使用操作与Integer一致,可以根据Java的官方文件来进行操作。
6.2.4 字符串类
在进行开发的过程中,我们经常遇到关于字符串的相关数据,这个时候我们需要将这些字符串信息表示到代码中,在Java的API中为我们提供了进行字符串处理的类,这个类是String,并且在该类中包含了大量用于字符串处理的方法,以方便于我们进行字符操作。
String类常用信息如下:

构造方法:
String() 无参构造方法,用来创建一个字符串对象,表示一个空字符序列
String(byte[] bytes) 通过平台的默认字符集指定的字节数组,构建一个新的字符串
String(StringBuffer buffer) 将字符串缓冲区中包含的字符转化为字符串
String(StringBuilder builder) 将字符串生成器中包含的字符转化为字符串

成员方法
char charAt(int index) 获取形式参数中指定的索引位置的单个char类型字符
int codePointAt(int index) 获取索引位置对应的字符在Unicode中编码
int length() 获取字符串的长度
boolean isEmpty() 判断字符串是否为空的,当且仅当length()返回值为0时,返回true
int compareTo(String anotherString) 按照计算机编码表来比较两个字符串,区分大小写
int compareToIgnoreCase(String str) 按照计算机编码表来比较两个字符串,不区分大小写
boolean equals(Object anObject) 比较两个字符串是否相等
boolean contains(CharSequence s) 判断当前字符串是否包含,形式参数中的字符
boolean startsWith(String prefix) 判断字符串是否以形式参数中的字符串开头
boolean endsWith(String suffix) 判断字符串是否以形式参数中的字符串结尾
int indexOf(int ch) 获取字符串中第一次出现形式参数指定的char字符的索引数值
int indexOf(String str) 获取字符串中第一次出现形式参数指定的字符串的索引位置
int lastIndexOf(int ch) 获取字符串中最后一次出现形式参数指定的char字符的索引位置
int lastIndexOf(String str) 获取字符串中最后一次出现形式参数指定的字符串的索引位置
String replace(char oldChar, char newChar) 在字符串中,通过newChar替换所有的oldChar
String substring(int beginIndex, int endIndex) 对字符串进行截取操作,从beginIndex开始,到endIndex结束,包含左侧但不包含右侧字符。
上面列出了对于字符串类,我们具有的相关功能和作用,下面我们使用代码演示一下,相关方法使用及功能介绍,代码如下:
public class StringTest {
//程序入口
public static void main(String[] args)
{

	//char charAt(int index) 获取形式参数中指定的索引位置的单个char类型字符
	String str1 = "ABCDEFGH";
	System.out.println( "指定索引位置的char字符:"+str1.charAt(1) );
	
	//int codePointAt(int index) 获取索引位置对应的字符在Unicode中编码
	System.out.println( "指定索引位置字符对应Unicode编码:"+str1.codePointAt(1) );
	
	//int length() 获取字符串的长度
	System.out.println( "字符串的长度为:"+str1.length() );

	//boolean isEmpty() 判断字符串是否为空的,当且仅当length()返回值为0时,返回true
	System.out.println( "str1时空的吗? :"+str1.isEmpty() );
	
	//int compareTo(String anotherString) 按照计算机编码表来比较两个字符串,区分大小写
	String str2 = "ABCD";
	String str3 = "abcd";
	System.out.println( "按照编码表的顺序比较两个字符串,区分大小写:"+str2.compareTo(str3) );
	
	//int compareToIgnoreCase(String str) 按照计算机编码表来比较两个字符串,不区分大小写
	System.out.println( "按照编码表的顺序比较两个字符串,区分大小写:"+str2.compareToIgnoreCase(str3) );

	//boolean equals(Object anObject) 比较两个字符串是否相等
	System.out.println( "两个字符串是否相等:"+str2.equals(str3) );
	
	//boolean contains(CharSequence s) 判断当前字符串是否包含,形式参数中的字符
	System.out.println( "str2中包含A:"+str2.contains("A") );
	

	String str4 = "HelloWorld.java";
	//boolean startsWith(String prefix) 判断字符串是否以形式参数中的字符串开头
	System.out.println( "str4是否为 He 开头:"+str4.startsWith("He") );
	
	//boolean endsWith(String suffix) 判断字符串是否以形式参数中的字符串结尾
	System.out.println( "str4是否为 .java 结尾:"+str4.endsWith(".java") );

	String str5 = "Java Android IOS JavaScript PHP";
	
	//int indexOf(int ch)  获取字符串中第一次出现形式参数指定的char字符的索引数值
	System.out.println( "str5中第一次出现 I 的位置是:"+str5.indexOf('I') );
	
	//int indexOf(String str) 获取字符串中第一次出现形式参数指定的字符串的索引位置
	System.out.println( "str5中第一次出现 Java 的位置是:"+str5.indexOf("Java") );
	
	//int lastIndexOf(int ch) 获取字符串中最后一次出现形式参数指定的char字符的索引位置
	System.out.println( "str5中最后一次出现 a 的位置是:"+str5.lastIndexOf('a') );
	
	//int lastIndexOf(String str) 获取字符串中最后一次出现形式参数指定的字符串的索引位置
	System.out.println( "str5中最后一次出现 Java的位置是:"+str5.lastIndexOf("Java") );

	//String replace(char oldChar, char newChar) 在字符串中,通过newChar替换所有的oldChar
	System.out.println( "使用JAVA替换Java:"+str5.replace("Java", "JAVA") );
	
	//String substring(int beginIndex, int endIndex) 对字符串进行截取操作,从beginIndex开始,到endIndex结束,包含左侧但不包含右侧字符。
	System.out.println( "str5进行截取操作:"+str5.substring(0,4) );

}

}
代码运行结果如图:

除了String类之外,我们还经常会使用StringBuilder、StringBuffer这两个类进行字符串的处理操作。StringBuffer类提供了常用的append和insert两个方法用来完成对于字符串的修改操作,该类支持在多线程场景下的字符串处理,是线程安全的。StringBuilder提供了一个可变长的字符串处理类,相对于String类每进行一次字符操作,都会创建相关字符串的变量在内存中,StringBuilder只在当前字符串上进行修改,更加节省内存空间占用。

6.2.5 日期类
在进行开发过程中,我们经常遇到需要对日期以及时间信息进行处理,在Java的API中为我们提供了很好的工具,用来进行时间信息的处理操作。这里我们介绍一下,常用的日期处理的类,其中Calendar类是一个抽象类,它用来处理某一个特定时间与对应的数据格式关系比如:YEAR、MONTH、DAY_OF_MONTH、HOUR等信息的对应操作,并提供了对于日历信息的转换操作功能,以及操作日历字段的功能。此外还有常用的日期类时Date,用来标识特定的瞬时,精确到毫秒,通过该类的相关功能我们可以获取对应的毫秒值,通过SimpleDateFormat这个类,可以对获取到的时间进行格式化操作,从而得到需要的不同的日历信息,可以是年月日时分秒等等的信息。
下面我们先讲解一下Calendar这个类对于时间的处理操作,主要包含成员变量、构造方法、成员方法等信息。
Calendar类的基础信息

成员变量:

static int YEAR 该字段表示日期中年份信息
static int MONTH 该字段表示日期中月份信息,对于Calendar获取月份是从0开始计数的
static int DATE 该字段表示当前是一个月的第几天
static int DAY_OF_WEEK 该字段表示一周的第几天,但是注意西方国家与我们对于周的计算不同,他们一周的第一天为周末!
static int DAY_OF_YEAR 该字段表示当年的第几天
static int WEEK_OF_MONTH 该字段表示当月的第几周
static int WEEK_OF_YEAR 该字段表示当年的第几周
static int HOUR 该字段表示当前的小时数
static int MINUTE 该字段表示当前的分钟数
static int SECOND 该字段表示当前的秒数

成员方法
static Calendar getInstance() 使用默认时区和语言环境获得一个日历信息
TimeZone getTimeZone 获取日历信息中的时区信息
int get(int field) 根据指定的字段信息获取对应字段的值
String toString() 获取该日历信息对应的字符串表示形式

TimeZone类
String getDisplayName() 获取默认区域的时区名称

下面我们使用代码演示一下相关API的使用,代码如下:
//当前类用来对Calendar类的相关功能进行测试
public class CalendarTest {

//程序入口
public static void main(String[] args)
{
	//Calendar是一个抽象类,通过getInstance方法获取该抽象类的实现类的对象
	Calendar cal = Calendar.getInstance();
	
	//static int YEAR 该字段表示日期中年份信息
	System.out.println( "现在是:"+cal.get(Calendar.YEAR)+"年" );
	
	//static int MONTH 该字段表示日期中月份信息
	//对于Calendar获取月份是从0开始计数的
	System.out.println( "现在是:"+(cal.get(Calendar.MONTH)+1)+"月" );
	
	//static int DATE  该字段表示当前是一个月的第几天
	System.out.println( "现在是:"+cal.get(Calendar.DATE)+"日" );
	
	System.out.println( "现在是:"+cal.get(Calendar.YEAR)+"年 "+(cal.get(Calendar.MONTH)+1)+"月 "+cal.get(Calendar.DATE)+"日" );

	//static int DAY_OF_WEEK  该字段表示一周的第几天,但是注意西方国家与我们
	//                        对于周的计算不同,他们一周的第一天为周末!
	System.out.println( "现在是:周"+cal.get(Calendar.DAY_OF_WEEK) );
	
	//static int DAY_OF_YEAR  该字段表示当年的第几天
	System.out.println( "现在是当年第:"+cal.get(Calendar.DAY_OF_YEAR)+"天" );

	//static int WEEK_OF_MONTH  该字段表示当月的第几周
	System.out.println( "现在是当月第:"+cal.get(Calendar.WEEK_OF_MONTH)+"周" );

	//static int WEEK_OF_YEAR  该字段表示当年的第几周
	System.out.println( "现在是当年第:"+cal.get(Calendar.WEEK_OF_YEAR)+"周" );
	
	//static int HOUR  该字段表示当前的小时数
	System.out.println( "现在是:"+cal.get(Calendar.HOUR)+"时" );

	//static int MINUTE 该字段表示当前的分钟数
	System.out.println( "现在是:"+cal.get(Calendar.MINUTE)+"分" );

	//static int SECOND  该字段表示当前的秒数
	System.out.println( "现在是:"+cal.get(Calendar.SECOND)+"秒" );
	
	//TimeZone  getTimeZone  获取日历信息中的时区信息
	//String getDisplayName() 获取默认区域的时区名称
	System.out.println( "时区名称:"+cal.getTimeZone().getDisplayName());

}

}
代码的运行结果如图:

上面的内容主要围绕着Calendar类的相关时间处理进行了操作,下面我们讲解一下Date类对于时间的处理,对于Date类来说,主要是针对瞬间时刻的毫秒值获取,我们可以通过SimpleDateFormat类,对时间进行格式化操作,从而使其更加利于查看时间相关的信息。
Date类的无参构造方法,可以用来创建一个Date类对象,并初始化时间值,该时间值是从基准时间,即 1970 年 1 月 1 日 00:00:00 GMT以来的毫秒值。
SimpleDateFormat类可以用来进行时间信息的格式化操作,在进行时间格式化操作时,我们需要使用不同的字符来表示不同时间信息含义,对于SimpleDateFormat类可以支持到的代表不同时间信息含义的字符,如下表所示。

字母 日期或时间元素 表示 含义
G Era 标志符 Text AD,平时我们说的公元
y 年 Year 年份,比如2019、2018
M 年中的月份 Month 月份信息:July; Jul; 07
w 年中的周数 Number 数字,表示一年中的周数
W 月份中的周数 Number 数字,表示一个月中的周数
D 年中的天数 Number 数字,表示一年中的天数
d 月份中的天数 Number 数字,表示一月中的天数
F 月份中的星期 Number 数字,表示一月中的周几
E 星期中的天数 Text 表示星期几,周几Tuesday; Tue
a Am/pm 标记 Text 表示上午AM,下午PM
H 一天中的小时数(0-23) Number 数字,表示一天中的时间点
k 一天中的小时数(1-24) Number 数字,表示一天中的时间点
K am/pm 中的小时数(0-11) Number 数字,表示一天中上午或下午时间点
h am/pm 中的小时数(1-12) Number 数字,表示一天中上午或下午时间点
m 小时中的分钟数 Number 数字,表示一小时中的分钟数
s 分钟中的秒数 Number 数字,表示一分钟内的秒数
S 毫秒数 Number 数字,表示一秒中的毫秒值
z 时区 General TimeZone 标准时区
Z 时区 RFC822 TimeZone RFC822时区格式

对于日常我们查看的时间来说,根据不同地域的习惯,存在不同的查看方式,下面为大家列出来,在SimpleDateFormat类中经常用来查看不同时间信息的字符组合形式,通过一定格式的字符组合,可以将Date获取的时间进行格式化,从而得到更加易于查看的时间格式,常用的时间格式组合如下表:

日期和时间模式 结果
“yyyy.MM.dd G ‘at’ HH:mm:ss z” 2019.07.31 公元 at 08:42:38 CST
“EEE, MMM d, ''yy” 星期三, 七月 31, '19
“h:mm a” 8:44 上午
“hh ‘o’‘clock’ a, zzzz” 08 o’clock 上午, 中国标准时间
“K:mm a, z” 8:45 上午, CST
“yyyyy.MMMMM.dd GGG hh:mm aaa” 02019.七月.31 公元 08:46 上午
“EEE, d MMM yyyy HH:mm:ss Z” 星期三, 31 七月 2019 08:46:49 +0800
“yyMMddHHmmssZ” 190731084736+0800
“yyyy-MM-dd’T’HH:mm:ss.SSSZ” 2019-07-31T08:48:04.184+0800

SimpleDateFormat类的常用方法有:
构造方法:
SimpleDateFormat(String pattern) 通过指定的时间日期格式化字符串来创建该类的对象。

成员方法:
String format(Date date) 将一个Date的对象中的时间信息,按照格式进行格式化操作
Date parse(String timeStr) 将表示时间的一个字符串按照相应的时间格式解析成Date类的对象
下面我们使用代码将当前的时间信息转换成不同的格式的时间信息,代码如下:
public class FormatTest {

//程序入口
public static void main(String[] args)
{
	
	//创建Date类的实例,并获取当前时刻距离基准时间的毫秒值
	Date time = new Date();
	
	//通过创建SimpleDateFormat类的对象,指定时间格式
	SimpleDateFormat format1=new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒");  
	SimpleDateFormat format2=new SimpleDateFormat("yy/MM/dd HH:mm");   
	SimpleDateFormat format3=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");  
	SimpleDateFormat format4=new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒 E ");  
	SimpleDateFormat format5=new SimpleDateFormat("一年中的第 D 天 一年中第w个星期 一月中第W个星期 在一天中k时 z时区");  

	//将当前时间根据指定的时间格式进行转换
	System.out.println(format1.format(time));
	System.out.println(format2.format(time));
	System.out.println(format3.format(time));
	System.out.println(format4.format(time));
	System.out.println(format5.format(time));
}

}
代码运行结果如图:

我们除了可以将当前的时间转换为字符串外,我们还可以将表示时间信息的字符串转为表示时间信息的Java类。下面我们使用代码将代表时间的字符串转换为日期类。代码如下:
public class StrToDateTest {

//程序入口
public static void main(String[] args) throws ParseException
{
	
	//在这里我们将一些具有一定格式的时间字符串,按照其格式解析为Date对象
	
	//表示时间信息的字符串
	String time1 = "2019年07月31日 13时12分01秒";
	String time2 = "19/07/31 13:12";
	String time3 = "2019-07-31 13:12:01";
	
	SimpleDateFormat format1=new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒");  
	SimpleDateFormat format2=new SimpleDateFormat("yy/MM/dd HH:mm");   
	SimpleDateFormat format3=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");  
	
	Date date1 = format1.parse(time1);
	Date date2 = format2.parse(time2);
	Date date3 = format3.parse(time3);
	
	//获取的数据是当前年份减去1900年的结果
	System.out.println( "年:"+date3.getYear() );
	System.out.println( "月:"+date3.getMonth() );
	System.out.println( "日:"+date3.getDay() );
	System.out.println( "时:"+date3.getHours() );
	System.out.println( "分:"+date3.getMinutes() );
	System.out.println( "秒:"+date3.getSeconds() );
	
}

}
代码运行结果如图:

注意这里获取的年份信息是使用当前的年份数据减去1900后得到的值,此外对于时间信息的直接获取我们更加常用的是Calendar类来完成操作,比较直接简便。

6.3 集合
集合是一个整体性的概念,当我们在编程的过程中如果需要处理多个数据,并且这些数据具有相同的数据类型,这个时候我们可以将这些数据存储到一个容器中,容器可以容纳多个相同类型的数据,Java中的集合就是一个很好的容器,提供了多个层次的API帮助大家应对于不同的数据情况进行存储。
6.3.1 Java中的集合类
Java中的集合类是一个针对处理多个数据时,提供的一个存储和处理的容器,针对于容器对于数据的存储等操作进行了详细的规划,主要包含对于数据的增、删、改、查、遍历等相关操作。此外Java中的集合是基于数据结构相关的理论知识而来的,针对于同一个容器采用不同的数据结构来存储数据,那么对于存储效率的影响是不同的。对于数据结构来说主要可以划分为两个方向进行讨论,主要为数据角度和结构角度,所谓的结构指的是在容器中各个数据之间的彼此关系,根据数据关系的不同我们可以划分为不同的逻辑结构,主要包括一对一结构、一对多结构、多对多结构。
在一对一的结构中主要包括,数组结构,这种结构具有索引表示,对于数据存储可以通过索引进行准确定位,但是一旦数组长度确定之后更改不容易,所以对于这种结构来说,具有增删慢、查找快的特点,在集合中ArrayList、Vector就是基于数组结构的。链表结构,这种结构通过定义一个Java类作为数据的节点,并在Java类中通过定义存储数据的成员变量以及引用位置的成员变量,可以通过节点的创建完成构建链表的操作,这个时候面临的问题是对于链表来说不具有索引,虽然增删一个元素很方便,但是由于只知道链表的表头,所以查询的时候比较耗费时间,在集合中LinkedList是基于链表实现的。栈结构,就像一个空的水杯,如果向里面放入一个一个的乒乓球,那么最先放入的那一个只能最后被取出,这种结构具有先进后出的特点,在集合中Stack就是基于这种结构来实现的。队列结构,就像其名称一样,是一个有序的队伍的感觉,对于先存入的元素必然也最早获取出来,具有先进先出的特点,在集合中Queue相关的Java类都是基于队列实现出来的。
在一对多的结构中我们主要讨论的是树,这种结构只有一个根元素,从根元素往下进行分支,每一层根据需要的情况可以进行划分,树结构利于查找和排序操作,在集合API中TreeSet、TreeMap<K,V>这些都是基于树结构来完成的。对于多对多结构主要指的是图结构,对于这种结构我们的一般操作时将其转化为树结构来进行处理。
除了基于结构的讨论之外,我们还会根据数据所具有的特性来讨论数据结构,所谓的数据主要指的是数据的具体内容,我们可以根据一定的计算步骤,得到每个数据的唯一特征值,这个值被我们称为哈希值或者散列值,这个值具有一定的唯一性,在容器的存储中可以将不同特征值的数据存储在一起,通过特征值进行区分,想HashSet、HashMap<K,V>等就是根据哈希特征来构建的API。
在Java的集合中,除了考虑作为容器怎么来存储数据,可以根据不同的结构层次以及数据特征来组织数据,同样也考虑到了,集合在多种场景下的应用时,应该注意的问题,比如当处于多线程的操作场景之时,集合需要支持到多线程的共同操作,不能出现线程安全的问题,那么为了解决这一类问题,在集合中专门加入了支持线程安全的锁机制,其中Vector就是支持多线程并发场景下的安全问题。此外Java还专门开辟了支持多线程操作的集合包,就是java.util.concurrent包,其中的ConcurrentHashMap<K,V>,ConcurrentLiknedQueue、ConcurrentSkipListMap<K,V>等等这些集合都可以支持到多线程下的数据操作。
此外我们可以从集合类的命名上可以看出,其基于的是哪些技术知识,在Java类的命名过程中,十分注重见名知意的特点,从其中我们可以看到像ArrayList、LinkedList、HashMap<K,V>、ConcurrentHashMap<K,V>这些类的名称中我们可以很直接的看到他们分别基于数组、链表、哈希、支持线程的这些特点。对于在编程过程中,如果我们遇到需要同时处理多个数据的场景下,可以根据数据所具有的的基本特征以及数据关系来记性不同集合API的选择,从而更加顺利的编写出程序代码。
6.3.2 Collection接口
Java中对于集合来说,Collection是一个比较大的集合分支,其主要用于直接存储相同类型的多个数据,不用去考虑数据时如何进行存储的。Collection接口是这一个集合体系分支的顶层接口,其规划的各个子类以及实现所必须具有的基础功能,比如必须存在一个空的无参构造方法,另外就是必修有一个单参数的用来创建对象的构造方法。此外还规划了对于集合来说常用的各种功能性方法,涉及到数据的增加、数据的删除、数据的判断包含、相等判断、迭代器操作等多种功能实现。作为底层接口很多的为子类规划好了具体的实现方向。
Collection集合下有两个比较重要的分支接口分别为List分支,还有就是Set分支,这两个分支根据其具有的不同特点各有侧重,其中List分支对于存储的数据时存在索引的,可以通过索引操作数据,存储到List分支集合中的数据,其存储顺序与数据的获取顺序是一致的,该分支允许重复数据的存在,并且可以允许多个null值的存在。其下属的三个常用实现类,分别为ArrayList基于数组实现,长度可以随意变换,被称为可变长的数组,Vector同样是基于数组实现,并且其具有支持多线程场景的适应能力,是一个线程安全的集合类,LikedList基于链表实现,操作灵活。对于Set分支来说,其不具有索引,故而不能支持到关于索引的相关操作,Set具有去重能力,可以直接对于重复元素进行去重操作,并且只能支持一个null的存储,该分支的数据存入顺序与数据的获取顺序是不同的,并且在每次获取的过程中其顺序都不一定相同。其分支下有两个比较常用的Java类,分别为HashSet基于哈希结构构造的集合,具有很好的去重能力,TreeSet基于树结构构造的集合,具有树具有的排序特性,存在在该集合中的元素,天然进行了排序操作。对于Collection中集合各自都有不同的特点,在使用过程中,需要根据具体的应用场景进行合理选择。接下来我们通过代码来验证上面这些集合所具有的不同特性。
接下来通过代码分别讲述一下各个集合类所具有的不同功能特点:
在List分支中,由于List是一个接口,不能创建实例,我们需要使用它的子类来完成相关的实例创建以及使用操作,这里我们选择了其中的一个子类,ArrayList通过该类的实例来完成集合类的功能验证操作。
在ArrayList中我们常用的方法有:
构造方法:
ArrayList() 构造一个初始容量为10的空列表
ArrayList(int count) 构造一个指定容量的空列表
成员方法:
boolean add(E e) 将指定的元素添加到集合列表的末尾
boolean contains(Object o) 判断集合列表中是否存在指定元素
E get(int index) 获取指定索引位置上的数据元素
boolean isEmpty() 判断当前列表是否为空的
E set(int index, E element) 将列表的指定索引位置的值设置为指定的值并将该索引位置对应的原来的值返回回来。
int size() 获取列表的长度
E remove(int index) 将列表中指定索引位置的元素进行移除
boolean remove(Object o) 将指定元素从列表中移除
void clear() 清空列表中的所有数据
Iterator iterator() 获取集合的迭代器
对于集合的迭代器Iterator,用于集合的遍历操作,其功能有:
boolean hasNext() 通过迭代器判断列表中是否存在下一个元素,存在返回true
E next() 通过迭代器获取列表中下一个元素的值
下面我们使用代码来演示一下ArrayList中常用方法的使用,代码如下:
//当前里的主要功能完成对于集合中的ArrayList类的API使用
public class ArrayListTest {

//程序入口
public static void main(String[] args)
{
	
	//创建ArrayList的实例,并指定存储String类型数据
	ArrayList<String> strs = new ArrayList<String>();
	
	//boolean add(E e) 将指定的元素添加到集合列表的末尾
	boolean result = strs.add("AAA");
	System.out.println( "元素是否添加成功:"+result );
	strs.add("BBB");
	strs.add("CCC");
	strs.add("AAA");
	
	System.out.println( "集合中的元素为:"+strs );
	
	System.out.println("====================");
	
	//boolean contains(Object o) 判断集合列表中是否存在指定元素
	result = strs.contains("BBB");
	System.out.println( "列表中是否包含元素BBB"+result );
	
	System.out.println("====================");
	
	// E get(int index) 获取指定索引位置上的数据元素
	String str = strs.get(2);
	System.out.println( "2号索引位置的元素为:"+str );
	
	System.out.println("====================");
	
	//boolean isEmpty() 判断当前列表是否为空的
	result = strs.isEmpty();
	System.out.println( "列表式是否为空:"+result );
	
	System.out.println("====================");
	
	// E set(int index, E element) 将列表的指定索引位置的值设置为指定的值
	//                             并将该索引位置对应的原来的值返回回来。
	
	String reMsg = strs.set(0, "MMM");
	System.out.println( "进行指定索引位置的数据更改:"+reMsg );
	
	System.out.println("====================");
	
	//int size() 获取列表的长度
	int len = strs.size();
	System.out.println( "列表的长度为:"+len );
	
	System.out.println("====================");
	
	//通过索引对集合进行遍历操作
	for(int i = 0 ; i < len ; i++)
	{
		System.out.println( "第"+(i+1)+"个元素为:"+strs.get(i) );
	}
	
	System.out.println("====================");
	
	//Iterator<E> iterator() 获取集合的迭代器
    //boolean hasNext() 通过迭代器判断列表中是否存在下一个元素,存在返回true
	//E next()   通过迭代器获取列表中下一个元素的值
	
	//获取指向strs集合的迭代器
	Iterator<String> it = strs.iterator();
	//通过迭代器遍历集合
	while(it.hasNext())
	{
		System.out.println( "元素:"+it.next() );
	}

}

}
代码运行结果如图:

从上面的代码以及运行结果中我们可以看到,对于List集合来说,是存在索引的,可以通过索引来进行操作,对于List集合来说,是有序的,这个有序指的是数据存入集合中的顺序与数据从集合中获取的顺序是一致的。

在Set的分支中,Set是一个接口,不具有创建实例的作用,我们需要选择其子类中的一个来完成功能的演示操作,这里我们选择HashSet这个类进行集合相关API的演示操作,对于HashSet来说,常用的API有:

构造方法
HashSet() 构造一个新的Set集合,其底层的HashMap实例初始大小为16
HashSet(int count) 构造一个新的Set集合,其底层的HashMap实例的初始长度为参数的大小

成员方法

boolean add(E e) 将指定的元素添加到集合列表的末尾
boolean contains(Object o) 判断集合列表中是否存在指定元素
boolean isEmpty() 判断当前列表是否为空的
int size() 获取列表的长度
boolean remove(Object o) 将指定元素从列表中移除
void clear() 清空列表中的所有数据
Iterator iterator() 获取集合的迭代器
对于集合的迭代器Iterator,用于集合的遍历操作,其功能有:
boolean hasNext() 通过迭代器判断列表中是否存在下一个元素,存在返回true
E next() 通过迭代器获取列表中下一个元素的值

我们使用Java代码演示一下对于HashSet类的使用:代码如下:
//定义Java类,用来测试HashSet中相关的功能
public class HashSetTest {

//程序入口
public static void main(String[] args)
{
	
	//创建HashSet<E>类的对象,并指定存储的数据类型为String
	HashSet<String> strs = new HashSet<String>();
	
	//boolean add(E e) 将指定的元素添加到集合列表的末尾
	boolean result = strs.add("AAA");
	System.out.println( "元素是否添加成功:"+result );
	strs.add("BBB");
	strs.add("CCC");
	strs.add("AAA");
	
	System.out.println( "集合中的元素为:"+strs );
	
	System.out.println("====================");
	
	//boolean contains(Object o) 判断集合列表中是否存在指定元素
	result = strs.contains("BBB");
	System.out.println( "列表中是否包含元素BBB"+result );
	
	System.out.println("====================");
	
	//boolean isEmpty() 判断当前列表是否为空的
	result = strs.isEmpty();
	System.out.println( "列表是否为空:"+result );
	
	System.out.println("====================");
	
	//int size() 获取列表的长度
	int len = strs.size();
	System.out.println( "列表的长度为:"+len );
	
	System.out.println("====================");
	
	//Iterator<E> iterator() 获取集合的迭代器
    //boolean hasNext() 通过迭代器判断列表中是否存在下一个元素,存在返回true
	//E next()   通过迭代器获取列表中下一个元素的值
	
	//获取指向strs集合的迭代器
	Iterator<String> it = strs.iterator();
	//通过迭代器遍历集合
	while(it.hasNext())
	{
		System.out.println( "元素:"+it.next() );
	}
	
}

}
代码的运行结果如图:

上面的集合示例中我们主要用来存储的是String类型数据,这里我们可以选择使用HashSet集合存储自定义的数据类型,此时需要注意一个问题,对于HashSet集合是具有去重操作的,而且是满足equals的重复,在我们进行自定义类的书写过程中,我们需要注意对于去重操作的支持,需要重写Java类中的hashCode以及equals方法,这两个方法继承自Object类,需要对其进行重写操作。下面我们自定义一个Java类,如:Student,我们将Student对象的相关数据存储到集合中。代码如下:
public class HashSetDemo {

//程序入口
public static void main(String[] args)
{
	
	//创建空的集合,用来存储自定义数据类型的对象
	HashSet<Student> stus = new HashSet<Student>();
	
	//定义多个Student对象,并且其中存在重复数据
	Student stu1 = new Student("张三",20,"男");
	Student stu2 = new Student("李四",21,"女");
	Student stu3 = new Student("王五",30,"男");
	Student stu4 = new Student("赵六",20,"女");
	Student stu5 = new Student("张三",20,"男");
	
	//将自定义数据类型的对象存储到集合中
	stus.add(stu1);
	stus.add(stu2);
	stus.add(stu3);
	stus.add(stu4);
	stus.add(stu5);
	
	//使用增强for来进行遍历集合
	for(Student stu : stus)
	{
		System.out.println( "元素:"+stu );
	}
}

}
代码运行结果如图:

从代码的运行结果中我们可以看到,对于自定义的Java类来说,同样完成了数据的去重操作,此时需要注意在自定义Java类中一定要重写hashCode与equals方法,否则不能完成去重操作。

6.3.3 Map接口
以Map接口为顶层接口的相关集合类,其主要针对的是在数据存储时,我们需要指定需要存储数据的键值,在Collection体系的集合中,我们存储数据不需要考虑该数据的相关标识问题,集合底层会自动完成数据的存储操作,并很好的进行组织。对于Map集合在我们向其中存储数据时,我们需要同时指定需要存储数据的键与值。从泛型角度来看,对于Collection体系集合来说,泛型表示需要存储的数据的类型,不需要关心该数据存储时的键问题。对于Map<K,V>体系集合来说,其中的K与V都是泛型,分别表示key与value的含义,在进行数据存储时,需要在存储数据的同时为其指定对应的键关系,用来在集合中唯一标识该数据。
对于Map<K,V>集合来说,我们比较常用的实现类有HashMap<K,V>、TreeMap<K,V>,他们都可以用来做键值对的数据存储。下面我们分别看一下两个实现类中常用的方法有哪些。
HashMap<K,V>类

构造方法
HashMap<K,V>() 无参构造方法,默认初始容量为16以及默认加载因子为0.75的空集合。
HashMap<K,V>(int count) 根据指定参数的个数,构造一个指定长度默认加载因子为0.75的空集合。

成员方法

V put(K key , V value)  根据指定的key与value,成对的向集合中添加元素
V remove(Object key)  根据指定的key删除对应的存储的键值数据
boolean containsKey(Object key)  判断参数中指定的key在集合中是否存在对应的键值对
boolean containsValue(Object value)  判断参数中指定的value是否在集合中存在对应的键值对
boolean isEmpty()  判断集合是否为空的
Set<K>  keyset()  获取集合中所有键的集合,存储到Set<K>中
V get(Object key)  根据指定的参数的key获取对应键的值

下面我们使用代码演示一下,上面方法的具体使用,代码如下:

public class HashMapTest {

//程序入口
public static void main(String[] args)
{
	
	//使用HashMap的无参构造,创建空的集合,并指定Key与Value的类型均为String
	HashMap<String,String> strs = new HashMap<String,String>();
	
	//V put(K key , V value)  根据指定的key与value,成对的向集合中添加元素
	strs.put("1", "AAAA");
	strs.put("2", "BBBB");
	strs.put("3", "CCCC");
	strs.put("4", "DDDD");
	strs.put("5", "AAAA");
	
	System.out.println( "集合中存储的数据是:"+strs );
	
	//boolean containsKey(Object key)  判断参数中指定的key在集合中是否存在对应的键值对
	boolean result = strs.containsKey("1");
	System.out.println( "strs中是否包含key值'1' :"+result );
	
	//boolean containsValue(Object value)  判断参数中指定的value是否在集合中存在对应的键值对
	result = strs.containsValue("AAAA");
	System.out.println( "strs中是否包含value值'AAAA' :"+result );
	
	//boolean isEmpty()  判断集合是否为空的
	System.out.println( "strs是否为空的:"+strs.isEmpty() );
	
	//Set<K>  keyset()  获取集合中所有键的集合,存储到Set<K>中
	//V get(Object key)  根据指定的参数的key获取对应键的值
	
	//通过上述两个方法的组合来完成,集合的遍历操作
	//获取集合中所有的key值信息
	Set<String> keys = strs.keySet();
	
	//遍历所有的key信息
	for(String key : keys)
	{
		//通过key获取对应value信息
		String value = strs.get(key);
		System.out.println( "key == "+key+" value == "+value );
	}
	
	//V remove(Object key)  根据指定的key删除对应的存储的键值数据
	strs.remove("2");
	System.out.println( "删除后的集合是:"+strs );
	
	
}

}
代码运行结果如图:

通过上面的代码我们演示了对于HashMap<K,V>中的各种功能的使用,由于TreeMap<K,V>与HashMap<K,V>都实现了Map<K,V>接口,两个类中具有的功能时很相似的,这就是在API中接口具有的功能规划,带来的好处。
此外我们通过Java的集合来完成一个实际需求,通过这个需求的实现来进一步了解集合的应用技巧。我们在Linux下,经常会使用一个命令find用来在指定目录下查找文件信息,并将对应文件名的相关绝对路径获取出来。下面我们通过Java代码来实现Linux中的命令find -name,通过文件名字从指定路径中查找该名称对应的所有文件的绝对路径。代码如下:
public class Test {

//对于需要存储信息的容器,需要存储的是 文件名称与文件的绝对路径
//这符合K,V存储的方式,
private static HashMap<String,HashSet<String>> name_paths = new HashMap<String,HashSet<String>>(); 

//程序入口
public static void main(String[] args)
{
	//对于完成 find -name
	//一、准备好需要查询的数据
	String dirPath = "C:\\AAAA";
	doJob(dirPath);
	
	//二、根据文件名获取对应的路径值
	String fileName = "a.txt";
	HashSet<String> paths = name_paths.get(fileName);
	
	//三、将名称对应的路径输出
	for(String path : paths)
	{
		System.out.println(path);
	}
}

//通过自定义方法完成目录的递归操作,并将递归到的文件信息存储到容器中
public static void doJob(String dirPath)
{
	//对于参数的判断
	if(null == dirPath)
	{
		System.out.println( "参数不能为null" );
		return;
	}
	
	//对于路径所代表的文件的检查
	File dirFile = new File(dirPath);
	
	if( !dirFile.exists()|| !dirFile.isDirectory() )
	{
		System.out.println( "对应的路径不存在或者不是目录" );
		return;
	}
	
	//获取该目录对应的下一层所有的文件
	File[] files = dirFile.listFiles();
	
	//使用增强for遍历数组
	for(File file : files)
	{
		//如果为文件,应该保存相关信息到容器中
		if(file.isFile())
		{
			String fileName = file.getName();
			String absPath = file.getAbsolutePath();
			
			//先判断该数据 是否在容器中已经存在
			if(name_paths.containsKey(fileName))
			{
				//获取该文件名称对应的路径信息
				HashSet<String> paths = name_paths.get(fileName);
				//添加名称对应的路径
				paths.add(absPath);
				//将名称与路径的集合重新存入
				name_paths.put(fileName, paths);
			}
			else
			{
				//容器中不存在历史数据时,创建新的容器,完成存入
				HashSet<String> newPaths = new HashSet<String>();
				
				newPaths.add(absPath);
				//将新的文件名称数据与路径数据存储到容器中
				name_paths.put(fileName, newPaths);
			}
		}
		
		if(file.isDirectory())
		{
			String absDirPath = file.getAbsolutePath();
			
			//调用方法本身完成递归操作
			doJob(absDirPath);
		}
	}
}

}
代码运行结果如图:

6.4 多线程编程
我们前面编写的程序都是单线程的处理逻辑,也就是说代码的运行会围绕着一条逻辑线一直走下去直到程序结束,中间如果遇到选择结构则会产生选择分支,遇到循环逻辑则会重复相应的操作多次,但是无论怎样,都是在一条逻辑线上执行的操作。这种程序的处理情况过于单一,当我们遇到需要并行处理的问题时,就会出现问题,这个时候需要使用我们的多线程技术来进行编程,使得程序的运行不仅仅只有一条逻辑线,可以同时运行多条,从而满足并行运行的目的。
6.4.1 多线程简介
对于线程来说,其主要是依附于进程而存在的,我们都知道Windows系统上有一个任务管理器,如果某个软件出现卡死的情况下,我们可以通过任务管理器的相关进程杀死,从而关闭该应用软件。进程可以说是操作系统管理软件的一个基本单位,一个应用软件可以对应一到多个进程,进程可以说是运行的程序或者软件以及支持其运行的CPU、内存、硬盘、网络等各项资源的一个统称,是一个资源的集合体,操作系统通过进程来管理相应的程序运行时所需要的各项资源。那么线程与进程又是一个什么关系呢?其实线程是进程的一个基本组成单位,进程的相关工作是由线程来直接完成的,对于进程所具有的的各项资源,线程同样具备,每一个进程都有一个负责工作的主线程,我们在前面编写代码时的main方法就是直接工作在主线程中,由于不做特殊操作时,我们的进程中只有一个主线程,所以我们编写的代码都运行在主线程中,并且是单线程进行处理的。总的来说,线程是进程的基本组成单位,每个进程都有一个主线程负责进行工作处理,当我们遇到需要进行并行操作的场景时,我们可以在主线程的基础上再创建其他线程,从而适应需求,构成多线程的处理,从而完成多线程的编程。
多线程编程时针对并发问题的处理,那么我们在平时的编程中有哪些多线程的场景呢?我们可以举几个例子,分析一下多线程的处理过程以及应用角度。比如我们前面在JavaAPI一节中的一个编程例子,通过网络编程,完成从客户端发送信息到服务端,服务端收到信息后进行回复。当时我们从客户端发送了Hello,客户端回复OK来完成的这个逻辑操作,这是没什么问题的,但是假如场景时这样的,多个客户端同时向服务端发送信息,并且要求服务端同时处理信息并回复。比如:客户端A,发送信息为张三,服务端回复张三你好,客户端B,发送信息为李四,服务端回复李四你好,客户端C发送信息为王五,服务端回复王五你好。如果是这样的场景下,那么服务端就需要并行的处理来自多个客户端的访问请求,并且多个请求之间是彼此独立的,互不干涉。这个时候在服务器端进行代码编写时,就需要使用到多线程的处理技术,从而应对并行操作的需求。
假设现在有这样的一个场景,需要将当前某个目录下的多个文件,完整的复制到另外一个目录下。这个时候我们可以分析一下需要怎么完成这件事,对于源路径下的各个目录以及文件我们都需要拷贝到另外一个目录下,并且各个文件的拷贝操作,应该彼此独立,不能一个文件拷贝完成之后再去拷贝另外一个文件,这样效率会很低,这种情况下我们可以使用多线程来解决并行拷贝文件的问题,对于每个文件都可以创建一个线程用来进行文件的拷贝操作,使得多个文件的拷贝工作可以同时进行,从而节省工作时间,提高工作的效率。
通过上面的举例我们解释了多线程的应用场景,在接下来的内容中,我们可以将上面的两个案例通过代码实现出来。
对于多线程来说,除了需要明确其应用场景以外,我们还需要知道线程的执行过程过程中的不同状态信息,方便我们更好的认识线程。对于线程来说,主要存在以下几个状态,就绪状态,对线程来说其中包含了需要运行的程序代码以及运行程序所需要的CPU、内存、硬盘、网络等各项资源,当除去CPU资源也就是CPU时间之外的其他资源都具备时,该线程处于就绪状态,只要获取到CPU时间片,该线程就可以开始执行。执行状态,当包含CPU时间片在内的所有资源都具备时,线程开始运行,并完成相对应的操作,这个状态称为执行状态。阻塞状态,当线程运行过程中,如果遇到某个操作,这个操作由于一些原因未能及时执行,这个时候该线程等待该操作完成才能继续后续的执行,这样的一个状态称为阻塞状态,比较常见的现象就是,我们使用Scanner进行录入信息的操作时,如果你没有从键盘键入信息,那么在控制台上,你会看到光标一直在闪烁,这个时候负责进行键盘录入信息的线程就处于了阻塞状态,只有在录入信息之后,该线程才会继续下去,除此之外我们在Java的I/O的编程中,当我们需要读取一个信息时,如果被读取的目标不存在,那么线程就会阻塞。睡眠状态,在线程的执行过程中,当我们调用线程的sleep方法时,会使线程进入睡眠状态,该状态下,线程暂停执行,在一定时间后回复执行,暂停的时间可以通过sleep方法的形式参数来指定。结束状态,线程按照既定的逻辑一直进行执行,最终得到结果,那么线程就会结束,亦或者由于错误等原因,导致线程无法继续运行,线程也会结束。
6.4.2 多线程实现的两种方式
在上一节中我们介绍了线程的基本概念,在Java中根据线程的理论提供了线程操作的Java类和接口,我们通过这些Java类和接口可以完成多线程代码的编写,从而解决多线程场景下的编程问题。对于线程的实现存在两种方式,一种利用Thread类来完成,一种是利用Runnable接口来完成,下面我们分别讲述两种线程的实现方式。
基于Thread类,实现Java的多线程,我们先列出Thread的常用成员信息如下:
构造方法
Thread() 无参构造方法,用来创建Thread类的实例
Thread( String name ) 创建Thread类的实例,并指定线程名称
Thread( Runnable target ) 利用Runnable的实现类的对象来创建Thread类的实例
Thread( Runnable target ,String name ) 利用Runnable的实现类的对象创建Thread类的实例,并且指定线程名称

成员方法
static Thread currentThread() 获取当前正在执行的线程的实例对象
long getId() 获取线程的编号
String getName() 获取线程的名称
void run() 从Runnable接口继承得到的方法,该方法中需要编写线程中执行的代码,否则线程什么操作也不会做。
void start() 开启线程时,需要调用此方法,在方法的执行过程中会调用run方法,从而执行run方法中编写的代码

通过继承Thread类实现线程我们需要经历以下几个步骤:
第一步:自定义一个Java类,继承Thread类
第二步:在自定义的类中,重写Thread类的run方法
第三步:在run方法中,编写需要在线程中进行处理的代码逻辑
第四步:使用自定义类的无参构造方法创建该类的实例
第五步:使用创建好的实例调用start方法,来启动线程

下面我们使用代码完成线程的实现,代码如下:
//自定义Java类继承自Thread
public class MyThread extends Thread{

//重写run方法
@Override
public void run()
{
	//在run方法编写相关代码,这里我们先不编写相关的业务处理代码
	//通过API获取当前正在执行的线程的基本信息
	Thread th = Thread.currentThread();
	
	//线程编号
	long threadId = th.getId();
	
	//线程名称
	String threadName = th.getName();
	
	System.out.println( "当前正在执行的线程:编号--"+threadId+" 线程名称:"+threadName );
			
}

}
public class Test {

//程序入口
public static void main(String[] args)
{
	//获取当前程序本身存在的线程信息
	//主线程信息
	
	//获取当前正在执行的线程
	Thread th = Thread.currentThread();
	
	//线程编号
	long threadId = th.getId();
	
	//线程名称
	String threadName = th.getName();
	
	System.out.println( "当前正在执行的线程:编号--"+threadId+" 线程名称:"+threadName );
	
	
	//使用自定义的Thread子类,完成线程的创建并运行
	
	//创建子线程的对象
	MyThread mt = new MyThread();
	
	//通过对象调用方法开启线程
	mt.start();
	
}

}
代码的执行效果如图:

基于Runnable接口,实现Java的线程,我们先看一下Runnable接口中我们需要使用到的成员信息:
void run()  在Runnable接口中定义的抽象方法,在子类实现该接口是,需要重写该方法,在其中编写线程中需要处理的代码逻辑。

通过实现Runnable接口,来完成线程的创建我们需要以下几个步骤:
第一步:编写一个自定义的Java类,实现Runnable接口
第二步:在实现类中重写run方法
第三步:在run方法的方法体中编写需要在线程中执行的代码
第四步:通过Thread类的有参构造Thread(Runnable target)创建对象
第五步:使用创建的对象调用start方法,开启线程

使用Runnable实现线程的代码如下:
//定义Java类实现Runnable接口
public class MyRunnable implements Runnable{

//重写run方法,并在方法中编写需要在线程中处理的逻辑
@Override
public void run() {
	
	//获取当前线程
	Thread th = Thread.currentThread();
	
	//获取线程编号
	long threadId = th.getId();
	
	//获取线程名称
	String threadName = th.getName();
	
	System.out.println( "线程名称为:"+threadName+" 线程编号:"+threadId );
}

}
public class Test {

public static void main(String[] args)
{
	//获取当前已经存在的主线程
	
	Thread th = Thread.currentThread();
	
	//线程名称
	String name = th.getName();
	
	//线程编号
	long id = th.getId();
	
	System.out.println( "线程编号:"+id+" 线程名字:"+name );
	
	
	//通过Thread(Runnable target)构造方法创建线程对象		
	MyRunnable mr = new MyRunnable();
	
	Thread thth = new Thread(mr);
	
	//开启线程执行
	thth.start();
	
}

}
代码的运行结果如图:

我们针对Java的线程来说,可以根据上面的实现逻辑来完成线程的创建,从而在程序的运行过程中具有了多线程的实例存在。我们可以使用具体的实例,来完成对于多线程的技术应用,我们通过下面两个例子,来掩饰一下多线程技术的具体使用。
在上一节内容中,我们提到了两个具体的编程场景,一、通过网络交互数据时,多个客户端向同一个服务端发送数据时,对于服务端来说,可以开启多线程来分别处理不同的访问请求。二、将某个目录下的文件复制到另外一个目录下,这个过程中对于每个文件的复制操作可以是单独的,并且多个文件之间的复制过程彼此独立,可以使用多线程来完成。

例一:在网络交互信息过程中,使用多线程来完成具体的编程处理
客户端代码
//客户端程序的类
public class Test {

//程序入口
public static void main(String[] args) throws UnknownHostException, IOException
{

	System.out.println( "这里是客户端" );
	//服务端的IP地址
	String ipAddress = "127.0.0.1";
	
	//服务端程序工作的端口号
	int port = 9999;
	
	sendData(ipAddress,port);
	
}

//自定义方法用来完成客户端向服务端发送信息并接收信息的回复
public static void sendData(String ipAddress, int port) throws UnknownHostException, IOException
{
	
	//根据地址与端口号,创建客户端到服务端的连接
	Socket sk = new Socket(ipAddress,port);
	
	//获取客户端到服务端的输出流
	OutputStream os = sk.getOutputStream();
	
	//构造数据输出流
	DataOutputStream dos = new DataOutputStream(os);
	
	//向服务端发送信息
	dos.writeUTF("张三");
	
	dos.flush();
	
	//客户端信息发送完成之后,等待服务端的回复
	
	//获取服务端到客户端的输入流
	InputStream is = sk.getInputStream();
	
	//构造数据输入流
	DataInputStream dis = new DataInputStream(is);
	
	//读取服务端回复的信息
	String recMsg = dis.readUTF();
	
	System.out.println( "收到服务端的信息回复"+recMsg );
	
	
	
}

}
服务器端代码
//作为服务端的程序入口类
public class Test {

//程序入口的main方法
public static void main(String[] args) throws IOException
{
	
	System.out.println( "这里是服务端程序" );
	
	//定义变量用来指定Socket服务端运行的端口号
	int port = 9999;
	
	ServerWork.reciveData(port);
}

}
//服务端程序处理的类
public class ServerWork {

public static void reciveData(int port) throws IOException
{
	
	//创建服务端并指定工作的端口号
	ServerSocket ss = new ServerSocket(port);
	System.out.println( "服务端程序开始运行" );
	
	//服务端需要一直处于工作状态,接收来自不同服务端的访问
	while(true)
	{
		//调用监听方法用来接收来自某个客户端的访问
		Socket sk = ss.accept();
		
		//针对服务端每次收到的连接开启单独的线程进行处理
		MyServerRunnable mr = new MyServerRunnable(sk);
		
		//使用Thread类的构造方法,创建实例
		Thread th = new Thread(mr);
		
		//开启线程
		th.start();
		
	}
	
}

}
//定义线程的处理类
public class MyServerRunnable implements Runnable{

private Socket sk;

public MyServerRunnable(Socket sk)
{
	this.sk = sk;
}

@Override
public void run() {
	
	try {
		//调用针对Socket连接进行处理的逻辑
		ServerJob.dealWithSk(sk);
	} catch (IOException e) {
		e.printStackTrace();
	}
	
	
}

}

//定义当前类,用来针对每次接收到的连接进行处理
public class ServerJob {

public static void dealWithSk(Socket sk) throws IOException
{
	//1、针对连接首先获取客户端发送的信息
	
	//获取针对Socket连接的输入流
	InputStream is = sk.getInputStream();
	
	//构造数据输入流,用来接收来自客户端的发送信息
	DataInputStream dis = new DataInputStream(is);
	
	//读取客户端发送的信息
	String msg = dis.readUTF();
	
	System.out.println( "服务端收到客户端的信息为:"+msg );
	
	//2、收到某个客户端的信息之后,可以针对该信息进行恢复
	
	//获取基于Socket连接的输出流
	OutputStream os = sk.getOutputStream();
	
	//构造数据输出流,用来向客户端回复信息
	DataOutputStream dos = new DataOutputStream(os);
	
	//给客户端回复信息
	dos.writeUTF( msg+"--OK" );
	
	dos.flush();
	
}

}
代码的执行效果如图:
运行一个客户端,发送信息:“张三”

运行另外一个客户端,发送信息:“李四”

从上面代码的执行效果我们可以看到,对于服务端程序来说,是一直运行着的,并且可以接受来自不同客户端的访问操作,在服务端收到某个Socket的连接请求之后,针对不同的连接开启单独的线程进行处理,在服务端的程序中构成多线程的运行环境。

例二:将某个目录下的多个文件以及目录拷贝到另外一个目录下
对于同一个目录下的多个文件来说,多个文件的拷贝操作时彼此独立的,可以使用线程来控制,让各个线程分别进行工作,从而更高效的拷贝文件。
代码如下:
//程序开始的类
public class Test {

//程序入口方法
public static void main(String[] args)
{
	
	//定义两个变量存储 源文件路径以及目的路径
	String fromPath = "C:\\AAAA\\SmallFiles";
	String toPath = "C:\\AAAA\\BB";
	
	//使用拷贝功能
	CopyUtil.doCopy(fromPath, toPath);
	
}

}
//自定义线程的类
public class MyThread extends Thread{

//定义变量保存需要拷贝的文件的源路径
private String fromFilePath = null;

//定义变量保存需要拷贝文件的目的路径
private String toFilePath = null;

public MyThread(String fromFilePath, String toFilePath)
{
	this.fromFilePath = fromFilePath;
	this.toFilePath = toFilePath;
}
@Override
public void run()
{
	
	try {
		CopyUtil.copyFile(fromFilePath,toFilePath);
	} catch (IOException e) {
		e.printStackTrace();
	}
	
	
}

}
public class CopyUtil {

//自定义方法完成目录的复制操作
public static void doCopy(String fromPath, String toPath)
{
	
	//进行参数的基础检查
	if(null == fromPath || null == toPath)
	{
		System.out.print( "传递到方法内的参数不能为空" );
		
		return;
	}
	
	//分别创建两个路径对应的文件对象
	File fromDir = new File(fromPath);
	File toDir = new File(toPath);
	
	if( !fromDir.isDirectory() || !toDir.isDirectory())
	{
		System.out.print( "两个文件对象对应的路径都应该是目录" );
		
		return;
	}
	
	copy(fromPath,toPath);
	
	
}

//自定义方法完成拷贝操作
private static void copy(String fromPath, String toPath)
{
	//定义文件对象指向该路径的目录
	File fromDir = new File(fromPath);
	
	//获取目录下对应的多个文件对象
	File[] files = fromDir.listFiles();
	
	//遍历获取到的文件对象
	for(File file : files)
	{
		//当遍历到的文件对象是一个普通文件时
		if(file.isFile())
		{
			//开启线程完成文件的拷贝操作
			String fromFilePath = file.getAbsolutePath();
			
			//需要拷贝到的位置的文件路径
			String toFilePath = toPath+"\\"+file.getName();
			
			MyThread mt = new MyThread(fromFilePath,toFilePath);
			
			mt.start();
		}
		
	}
	
}

//自定义方法完成单个文件拷贝
public static void copyFile(String fromFilePath, String toFilePath) throws IOException
{
	
	//定义文件的输入流
	FileInputStream fis = new FileInputStream(fromFilePath);
	
	//定义文件的输出流
	FileOutputStream fos = new FileOutputStream(toFilePath);
	
	int len = 0;
	byte[] buff = new byte[2048];
	
	//循环从源文件中读取数据,并写入到新文件中
	while((len = fis.read(buff))!=-1)
	{
		fos.write(buff, 0, len);
		fos.flush();
	}
	
	//关闭数据的流
	fos.close();
	fis.close();
}

}
通过上面的两个实例,我们从应用的角度展示了两种线程实现方式以及在实际开发中的应用角度,使用多线程最关键的就是区分好,主线程与子线程之间各自的代码逻辑,正确的编写在子线程中的代码逻辑,从而更好的提高程序的执行效率。
6.4.3 线程的属性和控制
在进行Java的编程过程中,我们对于线程会进行多方面的管理,对于线程来说,我们关注其多方面的信息,其中包含,线程编号、线程名称、线程优先级、Java虚拟机中线程的堆栈信息、线程的状态、线程所在线程组等属性信息,这些属性信息代表了Java线程由开始创建到执行以及到最后结束的整个过程中的信息,我们可以通过Java提供的API来获取这些相关的属性信息。
long getId() 获取当前正在执行的线程的标识符,一般是一个整数编号
String getName() 获取当前线程的名称,这个名称可以是系统分配的也可以是自己设置的
int getPriority() 获取正在执行的线程的优先级信息,在线程中优先级存在三个等级,分别是MAX_PRIORITY最高优先级,MIN_PRIORITY最低优先级,NORM_PRIORITY默认优先级。优先级代表了线程的执行先后。
StackTraceElement[] getStackTrace() 获取线程运行时,内存中堆栈信息,其中包括运行的类的名称,方法名称,本地方法,源文件信息等。
Thread.State getState() 获取线程的执行状态,其中主要包括以下几种状态,分别为:BLOCKED锁定状态,NEW线程新创建但为启动状态,RUNNABLE线程运行中的状态,TERMINATED线程终止的状态,TIMED_WAITING指定了线程等待时间的状态,WAITING线程等待状态。
ThreadFroup getThreadGoup() 获取当前线程所在的线程组信息。
我们可以通过上面的Java提供的相关API来获取线程的基本属性信息,在线程的开发过程中我们还需要对线程进行控制,比如:线程启动、线程睡眠、线程的暂停、线程的停止等操作,通过这些操作我们控制线程的整个执行过程。
void start() 用来完成线程的开启操作,在线程开启之后,进入启动状态,获取CPU时间后开始执行线程,进入执行状态。
void destroy() 线程销毁方法,不过该方法存在缺陷,破坏线程的执行过程中,不会做线程相关的资源清除操作。
void interrupt() 中断线程的执行,可以用来停止某个线程的执行操作。
void stop() 该方法用来完成线程的终止操作,但是该方法存在不安全性,该操作会释放已经锁定的所有监视器,受监视器保护的对象数据会出现不一致性。
void yield() 暂停当前正在执行的线程,将系统资源让给其他线程使用,执行其他线程。
对于stop方法与destory方法不推荐使用,可以通过中断线程或者暂停线程的方式来停止当前正在执行的线程。利用线程的基本控制以及属性获取方法,我们就可以了解更多线程执行中的信息,方便对线程进行管理,合理的编写多线程代码。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值