Java 常用API

String是不变对象,即:字符串对象创建后,表示的字符内容不可变,若改变必定创建新对象
java对于字符串有一个优化,即:
字符串常量池,这是在堆内存中开辟的一块空间,用于保存所有使用字面量形式创建的字符串对象
当再次使用该字面量创建新的字符串时会直接重用而不会再创建新的以达到节省内存开销的目的

boolean tf=s2.equals(s4);
通常我们比较字符串都是比较字符串的内容,因此比较字符串时我们不会使用"=="而是使用字符串的方法equals.
equals方法是用来比较两个字符串对象所表示的内容是否相同的

String s5="123"+"abc"
这里用到了编译器的一个特性:
编译器在编译源代码时,当遇到一个计算表达式可以在编译期间确定结果时就会直接进行计算,并将结果编译到class文件中,
这样JVM每次运行程序时就无需再计算
比如: int a= 1+1; 编译器编译后class改为: int a=2;
下面的字符串也是如此,会被改为: String s5="123abc";

String substring(int start,int end)
截取当前字符串中指定范围内的字符串.注:java API中,通常使用两个数字表示范围时,都是含头不含尾的
String sub=line.substring(4, 8);//含头不含尾
sub=line.substring(4);//从指定位置开始截取到字符串末尾

String toUpperCase()
String toLowerCase()
将当前字符串中的英文部分转换为全大写或全小写

String trim()
去除当前字符串两侧的空白字符

char charAt(int index)
获取当前字符串中指定位置对应的字符

int indexOf(String str)
查找给定字符串在当前字符串中的位置,若当前字符串不包含给定内容则返回值为-1(可利用这个特点查找字符串中
是否含有想要的字符,没有则返回-1)

int indexOf(String str, int index)
重载的方法允许我们从指定位置开始查找第一次出现的指定字符的位置

index=str.lastIndexOf("in");
查找最后一次出现指定字符串的位置
可利用indexOf 和 lastIndexOf 判断某个字符是否只出现一次,同一个位置则只出现一次

int len=str.length();
获取当前字符串的长度(字符个数)

boolean matches(String regex) String支持正则表达式的方法之一
使用给定的正则表达式匹配当前字符串是否符合格式要求,符合则返回true
注意:给定的正则表达式就算不指定边界匹配符即:(^...$)也是做完全匹配验证的

String[] split(String regex) String支持正则表达式方法二
将当前字符串中按照满足正则表达式的部分拆分,然后将拆分后的字符串以数组形式返回

String replaceAll(String regex,String str) String支持正则表达式方法三
将当前字符串中满足正则表达式的部分替换为给定的内容 line=line.replaceAll("[a-zA-Z]+", "#NUMBER#");

boolean startsWith(String str)
boolean endsWith(String str)
判断字符串是否是以给定的字符串开始或结尾的

java.lang.StringBuilder
1.String的优化措施仅照顾重用性,因此频繁修改字符串会带来内存开销大,运行效率差的结果对此,
java提供一个专门用于修改字符串的API
2.其内部维护一个可变的char数组,所有的修改都是在这个数组中进行的,因此开销小,性能好,并且其提供了
便于修改字符串的一系列方法,包括了增,删,改,插等基本操作
builder.append(",为了找个好工作!"); //插入 ",为了找个好工作!"
str=builder.toString();//获取StringBuilder内部的字符串
builder.replace(9, 16, "就是为了改变世界"); //好好学习java,为了找个好工作!"修改为 "好好学习java,就是为了改变世界!"
builder.delete(0, 8); // "好好学习java,就是为了改变世界!"删除"好好学习java"
builder.insert(0, "活着"); // ",就是为了改变世界!" 插入"活着,就是为了改变世界!"

static String valueOf(XXX xxx)
字符串提供了若干的重载的valueOf方法,它们都是静态方法.作用是将给定的内容转换为字符串
str=123+""; //任何类型与字符串连接结果都是字符串


包装类
包装类是为了解决基本类型不能直接参与面向对象开发的问题
8个基本类型对应8个不同的包装类,其中6个表示数字的包装类继承自java.lang.Number,其他两个继承自Object

Integer i1=Integer.valueOf(d);
java推荐我们使用包装类的静态方法:valueOf来将基本类型转换为包装类

int i=i1.intValue();
包装类转换为基本类型

int max=Integer.MAX_VALUE;
int min=Integer.MIN_VALUE;
数字类型的包装类有两个常量,MAX_VALUE,MIN_VALUE分别记录了其对应的基本类型的范围

String str="22";
int a=Integer.parseInt(str);
包装类提供了一个功能,静态方法parseXXX
该方法可以将字符串解析为对应的基本类型数据
前提是该字符串能正确描述要转换的基本类型可以保存的值,异常是 NumberFormatException

自动拆装箱
JDK5之后推出了一个新的特性:自动拆装箱
该特性是编译器认可的,而不是虚拟机,编译器在看到有基本类型与包装类之间互相赋值时
会添加转换代码将他们转换
int i=new Integer(1); //这里触发了编译器的自动拆箱特性:
//代码会被编译器改为: int i=new Integer(1).intValue();
Integer in=1; //触发自动装箱特性,代码会被改为: Integer in =Integer.valueOf(1);

input ouput

java.io.File File用于表示文件系统中的一个文件或目录
使用File我们可以:
1.访问其表示的文件或目录的属性(名字,大小等)
2.创建,删除文件或目录
3.访问一个目录中的子项
但是不能访问文件数据

创建File是要指定路径
路径有两种:绝对路径和相对路径
绝对路径通常不适用,虽然清晰明了但是无法做到跨平台
相对路径不能直观体现出实际位置,但是灵活并适应各种不同运行环境
在eclipse中执行代码时,"./"指的就是当前项目目录

File file=new File("./demo.txt");//在当前项目目录下新建一个文件demo.txt
String name=file.getName();//获取文件名
long len=file.length();//获取长度(单位是字节),返回值是long型
String path=file.getAbsolutePath();//获取绝对路径
boolean cr=file.canRead();//是否可读
boolean cw=file.canWrite();//是否可写
boolean ih=file.isHidden();//是否为隐藏文件
boolean exist=file.exists()//判断当前File表示的文件或目录是否已经存在,存在则返回true
file.createNewFile();//创建该文件,增加一个判断if(!file.exists()),不存在再创建
file.delete();//删除一个文件,增加一个判断if(file.exists()),存在再删除

File dir=new File(".");//"."表示当前目录下
boolean isFile()
判断当前file表示的是否文件
boolean isDirectory()
判断当前File表示的是否为目录
File[] listFiles()
获取当前目录中的所有子项,数组中每一个元素就是其中一个子项
File[] subs=dir.listFiles();// if(dir.isDirectory())判断是否为目录,是再获取当前目录中的所有子项

File[] listFiles(FileFilter filter)
将目录中符合过滤器需求的子项获取

File dir=new File(".");
FileFilter filter = new FileFilter() {
public boolean accept(File file) {//重写FileFilter接口中的accept方法
return file.getName().startsWith(".");//返回当前目录中所有名字以"."开头的内容
}
}; //匿名内部类定义过滤器
File[] subs=dir.listFiles(filter);//回调模式,会调用匿名内部类中的方法

File dir=new File("./demo");//在当前目录下新建一个名为demo的目录
if(!dir.exists()) { //判断目录是否存在
dir.mkdir();} //创建新目录

File dir=new File("./a/b/c/d/e/f");//在当前目录下新建目录:a/b/c/d/e/f
if(!dir.exists()) { //判断多级目录是否存在(判断到最后一级/f)
dir.mkdirs();}//创建多级目录
if(dir.exists()) { //判断多级目录是否存在(判断到最后一级/f)
dir.delete();} //删除目录有一个前提条件就是该目录是一个空目录,只能删一个,本例/f目录没数据才删掉/f,上层目录没删

java.io.RandomAccessFile
RAF是专门用来读写文件数据的API,其基于指针对文件任意位置进行读写

RandomAccessFile raf = new RandomAccessFile("./raf.dat","rw");
对当前目录下的raf.dat文件读写数据 String path 权限

void write(int d)
向文件中写入1个字节,写入的是给定的int值所对应的2进制的"低八位"
vvvvvvvv
00000000 00000000 00000000 00000001
raf.write(257);//只会存二进制"低八位" 取出来十进制值为1


int read()
从文件中读取一个字节,并以int形式返回.若返回值为-1则表示为文件末尾

long pos=raf.getFilePointer();//获取指针位置,返回long值

向文件中写入一个int最大值,用右移运算符,把高位移到低八位,保存4个字节,即可把一个4字节的int值保存了
void writeInt(int d);//将给定的int值对应的4字节一次性写出,等同上面的操作
raf.writeLong(213L);
raf.writeDouble(345.343);

void seek(long pos) //移动指针到指定位置
raf.seek(0);

int readInt()
连续读取4个字节并返回该int值,若连续读取4个字节的过程中发现读取到了文件末尾,
此时会直接抛出异常EOFException. EOF(end of file)文件末尾
int d1=raf.readInt();
long l=raf.readLong();
double dou=raf.readDouble();

String提供了转换为字节的方法
byte[] getBytes() //将当前字符串按照系统默认字符集转换为一组字节
byte[] getBytes(String csn) // csn------>比如:GBK,UTF-8
重载的getByts方法要求我们传入一个字符串参数表示字符集的名称,该名称不区分大小写,
作用是将当前字符串按照给定的字符集转换为一组字节,推荐用这种方法,不要按照系统默认字符集操作
line.getBytes("utf-8");

使用RAF向文件中写入字符串
raf.write(",能否听清,".getBytes("UTF-8"));

单字节读写是随机读写,效率差
块读写是一组一组字节的读写,效率高 通过提高每次读写的数据量,减少实际读写的次数,可以提高读写效率

int read(byte[] data)
一次性从文件中读取给定数组总长度的字节量,并将读取到的数据存入到该数组中,返回值为实际读取到的字节量
若返回值为-1,则表示文件末尾(本次没有读取到任何数据)

void write(byte[] data)
一次性将给定字节数组中所有字节写入文件
void write(byte[] data,int s,int len)
将给定字节数组从下标s处开始的连续len个字节一次性写入文件
byte[] data=new byte[1024*10];//10K每次
int len=-1;//len保存每次实际读取到的字节数
while((len=src.read(data))!=-1) {
desc.write(data,0,len);
}

java io 标准的输入与输出
使用java IO我们可以对外界设备以相同的方式进行读写完成数据交换

java IO将"读"与"写"按照方向进行了划分:
输入:从外界到程序的方向,用于让程序获取外界数据,因此输入是"读"数据的操作
输出:从程序到外界的方向,用于将数据"写"出的操作

java IO以"流"的形式表示读写功能
java.io.InputStream 输入流,通过输入流我们可以连接上外界设备从而读取该设备数据
java.io.OutputStream 输出流
以上两个流是所有字节输入流和输出流的超类,规定了所有输入流与输出流的基本读写功能

java将流分为两大类:节点流与处理流
节点流:又称为"低级流",是真实连接程序与数据源的"管道",用于实际搬运数据的流.读写一定是建立在节点流的基础上进行的
处理流:又称为"高级流",高级流不能独立存在,必须连接在其他流上,目的是当数据流经当前流时对其做某些加工处理,简化我们读写
数据时的相应操作

实际使用IO时,我们通常会串联若干的高级流最终连接到低级流上,使得读写数据以流水线式的加工处理完成,这个操作称为"流的连接",
也是IO的精髓所在

文件流
文件流是一对低级流,作用是连接到文件上,用于读写文件数据.
java.io.FileOutputStream:文件输出流
java.io.FileInputStream:文件输入流
文件流提供的构造方法:
FileOutputStream(File file)
FileOutputStream(String path)
以上两种创建方式,默认为覆盖写模式,即:若指定的文件已经存在,那么会将该文件原有数据全部删除,然后再将新数据写入文件
FileOutputStream(File file,boolean append)
FileOutputStream(String path,boolean append)
以上两种构造器允许再传入一个boolean值类型的参数,如果该值为true时,文件输出流就是追加写模式,
即数据中原有数据都保留,新内容会被追加到文件末尾
写文件
FileOutputStream fos=new FileOutputStream("./fos.txt",true);
fos.write("干".getBytes("UTF-8"));
读文件
FileInputStream fis=new FileInputStream("fos.txt");
byte[] data=new byte[1000];
int len=fis.read(data);
String str=new String(data,0,len,"UTF-8");

文件流与RAF的区别:
RAF是基于指针的随机读写形式,可以对文件任意位置进行读或写操作,可以做到对文件部分数据覆盖等操作,读写更灵活
文件流是基于java IO的标准读写,而IO是顺序读写模式,即:只能向后写或读数据,不能回退
单从读写灵活度来讲RAF是优于文件流的,但是文件流可以基于java IO的流连接完成 一个复杂数据的读写,这是RAF不容易做到的

缓冲流
java.io.BufferedOutputStream
java.io.BufferedInputStream
缓冲流是一对高级流,在流连接中的作用是提高读写效率,使得我们在进行读写操作时用单字节读写也能提高读写的效率
缓冲流之所以可以提高读写效率,是因为缓冲流内部有一个缓冲区(一个字节数组),无论我们使用缓冲流进行何种读写
(单字节或块读写),最终都会被缓冲流转换为块读写来提高效率
int d=-1;
while((d=bis.read())!=-1) { //读到文件末尾,返回-1
bos.write(d);
}

void flush()
flush方法是OutputStream中定义的方法,所有的输出流都具有该方法,但是是有缓冲流的该方法有实际意义.
其他的流具有该方法的目的是在流连接中传递缓冲操作给缓冲流
flush的作用是将缓冲流中已经缓存的数据一次性写出.
频繁调用flush方法会提高写出的次数从而降低写出效率,但是能保证数据写出的即时性

对象流
java.io.ObjectOutputStream
java.io.ObjectInputStream
对象流是一对高级流,在流连接中的作用是方便读写java对象.(对象与字节的转换由对象流完成)
该方法可能抛出:NotSerializableException
当写出的对象所属的类没有实现Serializable接口时就会抛出该异常
写入文件后发现该文件的实际数据量比当前对象保存的内容要大,这是因为这组字节除了包含了该对象的数据外
还有这个对象的结构信息
Serializable 序列化接口
private static final long serialVersionUID = 1L;//手动定义ID值
private transient String[] otherInfo;
当一个属性被transient修饰后,该对象在序列化时会忽略此属性的值
忽略不必要的属性值可以达到对象序列化瘦身的效果,减少不必要的资源开销
Person p=(Person)ois.readObject();//readObject()返回Object,需要强制转型

字符流
java.io.Reader和java.io.Writer
上述两个类是所有字符流的超类,规定了所有字符流都必须具备的读写字符的相关方法
java将流按照读写单位划分为字节流和字符流.
字符流以char为单位读写数据,但是底层本身还是读写字节,只是字符与字节的转换字符流自行完成

转换流
java.io.InputStreamReader
java.io.OutputStreamWriter
转换流是一对字符流,同时他们也是高级流,在实际开发时我们通常不会直接操作这两个流,但是在读写文本数据时,
流连接中他们是非常重要的一环,负责衔接其他字符高级流与字节流
FileOutputStream fos=new FileOutputStream("osw.txt");
OutputStreamWriter osw=new OutputStreamWriter(fos,"UTF-8");
osw.write("直到所有的灯都熄灭了也不停留");
转换流在创建时通常会传入第二个参数,这个参数用来指定字符集,这样一来通过当前流写出的文本数据
都会按照该字符集转换为字节后写出
FileInputStream fis=new FileInputStream("osw.txt");
InputStreamReader isr=new InputStreamReader(fis,"UTF-8");
int d=-1;
while((d=isr.read())!=-1) { //读到文件末尾,返回-1
System.out.print((char)d);// 可以一次读一个字符
}

缓冲字符流
java.io.BufferedWriter
java.io.BufferedReader
缓冲流是块读写文本数据,提高读写效率,并且可以按行读写字符串
java.io.PrintWriter具有自动刷新的缓冲字符输出流,内部总是连接BufferedWriter作为其缓冲加速操作

PrintWriter pw=new PrintWriter("pw.txt","UTF-8");
pw.println("让我掉下眼泪的,不止昨夜的酒");
向pw.txt文件中写入字符串,该构造方法内部会自动进行流连接操作,分别连接缓冲字符流,转换流和文件流

FileOutputStream fos=new FileOutputStream(fileName);
OutputStreamWriter osw=new OutputStreamWriter(fos,"UTF-8");
BufferedWriter bw=new BufferedWriter(osw);
PrintWriter pw=new PrintWriter(bw,true);//true 打开行自动刷新
创建PrintWriter时使用的构造方法如果第一个参数是一个流,那么就支持第二个boolean参数,
这个参数是 一个boolean值,该值为true时就打开了自动行刷新功能,此时每当调用println方法写出一行字符串都会自动flush

缓冲字符输入流
java.io.BufferedReader
块读文本数据,并且可以按行读取字符串
String str=null;
while((str=br.readLine())!=null) {
System.out.println(str);
}
String readLine();
连续读取若干字符直到读取到换行符为止
将换行符之前的字符组成一个字符串返回.如果返回值为null表示流读取到末尾了

 

正则表达式:记录文本规则的代码
[abc]:a b c中任意一个字符 [^abc]除了a b c的任意一个字符 [a-zA-Z0-9] [a-z&&[^bd]]
. :任意一个字符 \d:相当于[0-9](在java代码中须写成"\\d") \w:相当于[a-zA-Z0-9_]
\s:空白字符(比如\t\n 空格) \D: 非\d \W: 非\w \S: 非\s
?:表示0或1个 *:表示0~任意多 +:表示任意多
{n}:表示有n个 {n,}:表示从n~任意多 {n,m}:表示n~m个
():表示分组,用()|()|()表示"或"关系
^:表示字符串开始 $:表示字符串结尾

异常处理机制
java异常处理机制中的try-catch
try{
程序代码片段
}catch(XXXException e){
当try中出现XXXException后的解决代码
}finally{ //有必要的时候加入
finally块是异常处理机制中的最后一部分,他可以直接跟在try语句块之后或者最后一个catch块之后
finally可以保证只要程序执行到try当中,无论是否出现异常,finally中的代码都必定执行
通常我们可以将释放资源这样的必须操作的代码放在这里
JDK7之后推出了一个特性:AutoCloseable
该特性旨在让我们在源代码中可以以更简化的代码完成在finally中关闭流
}
try(
/*
* 在这里定义的流最终会被编译器改为在finally中关闭
* 只有实现了AutoCloseable接口的类才能在这里定义并实例化
* 流和RandomAccessFile都实现了该接口
*/
FileOutputStream fos=new FileOutputStream("fos.txt");
){
fos.write(1);
}catch(Exception e) {
System.out.println("出错了");
}
当JVM执行程序时发现某个错误时就会实例化对应的异常实例并将程序执行过程设置好然后将异常抛出
此时若没有异常处理机制,该异常会被继续抛出到当前方法之外(这里就抛出到main方法外)若最终抛给虚拟机,则会直接中断
catch是可以定义多个的,针对try中出现的不同异常有不同处理方式时可以分别捕获它们
可以在最后一个catch处捕获Exception来避免因为一个未捕获的异常导致程序的中断

面试中finally常见问题
请分别说明final,finally,finalize

finalize是Object定义的方法,该方法是当GC释放该对象资源时调用此方法,调用后该对象即被释放
注意:此方法若重写,里面不应当有耗时的操作
e.printStackTrace();// 将当前的错误信息输出到控制台上
String message=e.getMessage();// 获取错误信息


8.1

java.net.Socket 套接字
Socket封装了TCP协议传输数据的细节,使得我们可以通过两条流的读写完成与远端计算机的数据交互

socket=new Socket("localhost",8088); // localhost代表本机IP,端口一般选择8000以后的
实例化Socket的过程就是连接服务端的过程
* 参数1:服务端的IP地址
* 参数2:服务端程序打开的端口
我们通过IP可以找到网络上的服务端所在计算机,通过端口可以找到该计算机上运行的服务端应用从而建立连接

运行在服务端的ServerSocket主要有两个作用
1:向系统申请服务端口,客户端就是通过这个端口与服务端程序建立连接的
2:监听服务端口,一旦一个客户端建立连接就会返回一个Socket实例,服务端就可以通过这个Socket实例与该客户端交互了

server=new ServerSocket(8088);
实例化ServerSocket时要指定申请的服务端口,如果该端口被系统的其它程序使用时会抛出异常

Socket socket=server.accept();
Socket accept() 该方法是一个阻塞方法,调用后就"卡住了",此时开始等待客户端的连接,一旦一个客户端建立连接,
此时该方法会立即返回一个Socket实例,通过这个Socket就可以与该客户端交互了

OutputStream getOutputStream()
OutputStream out=socket.getOutputStream();
Socket提供的方法,通过该方法获取的输出流写出的字节会通过网络发送给远端计算机

InputStream getInputStream()
InputStream in=socket.getInputStream();
socket的该方法获取的输入流读取的是远端计算机发送过来的字节


while((message=br.readLine())!=null) {}
当客户端断开连接时,服务端这边readLine方法有两种情况:
1.返回值为null,通常Linux的客户端断开时会出现这种情况
2.直接抛出SocketException:connection reset

8.2
多线程
多线程改变了代码的执行方式,从原有的所有代码都串行操作改变为多个代码片段之间并行操作.
因此多线程允许多个代码片段"同时运行"

创建线程的方式有两种:
1.继承线程并重写run方法,在run方法中定义线程要执行的任务.
启动线程时调用线程的start方法而不是直接调用run方法.当线程的start方法调用后,线程纳入到
线程调度中,当其第一次分配到的时间片开始运行时,它的run方法会自动被执行
这种方式优点是创建简单方便.但是缺点也比较明显:
1.由于java是单继承的,这导致继承了线程就无法在继承其他的类,这会导致无法重用其他超类
的方法而产生继承冲突问题.
2.定义线程的同时重写run方法,这就等于规定了线程要执行的具体任务,导致线程与其执行的任务
产生必然的耦合关系,不利于线程的重用
2.实现Runnable接口并重写run方法来单独定义线程的任务
单独实例化任务,创建线程(传入实例化任务对象的引用)

static Thread currentThread()
线程提供了一个静态方法,该方法可以获取运行这个方法的线程

实际上java的所有代码都是靠线程运行的,main方法也不例外.
运行main方法的线程不是由我们创建的,而是JVM自行创建的,并用来运行main方法.
而我们通常称这个线程为"主线程"

ClientHandler handler=new ClientHandler(socket);// 启动一个线程处理该客户端交互

String host=socket.getInetAddress().getHostAddress();// 通过socket获取远端计算机地址信息

Thread main=Thread.currentThread();//获取当前线程
String name=main.getName();//获取线程的名字
long id=main.getId();//获取唯一标识
int priority=main.getPriority();//获取线程的优先级
boolean isAlive=main.isAlive();//是否活着
boolean isDaemon=main.isDaemon();//是否被守护
boolean isInterrupted=main.isInterrupted();//是否被打断

线程的优先级
* 线程启动后就纳入到了线程调度中统一管理,什么时候获取CPU时间片 完全取决于线程调度.
* 线程是不能主动索取的,通过调整线程的优先级可以最大程度的干涉分配CPU时间片的几率
*
* 理论上线程优先级越高的线程获取CPU时间片的几率越高
*
* 线程的优先级有10个等级,用整数1~10表示
* 1是最小的,5是默认,10是最高的
min.setPriority(Thread.MIN_PRIORITY);
max.setPriority(Thread.MAX_PRIORITY);

static void sleep(long ms) 线程提供了一个静态方法,该方法可以让运行这个方法的线程处于阻塞状态
指定的毫秒,超时后线程会自动回到RUNNABLE状态再次并发运行
Thread.sleep(5000);//阻塞5s
sleep方法要求处理中断异常
当一个线程调用sleep方法处于阻塞状态的过程中,此时被其他线程调用了该线程
的interrupt方法,那么就会打断这个线程的睡眠阻塞,此时sleep方法就会抛出中断异常告知

守护线程
守护线程也称为后台线程,创建和使用上与前台线程一样,但是有一点不同:
当进程结束时,所有正在运行的守护线程都会被强制停止
进程的结束:当所有普通线程都结束时,进程结束
设置为守护线程要在线程启动前进行 setDaemon(true);//记得主线程的状态
//GC是一个典型的守护线程

join方法允许当前线程在join方法所属线程上等待,直到该线程结束后结束join阻塞继续后续操作
所有join方法可以协调线程的同步运行 //download.join();
同步运行:多个线程之间实行有顺序
异步运行:多个线程之间各自执行各自的

当一个方法的局部内部类中引用了这个方法的其他局部变量时,该变量必须声明为final的
JDK8之后可以不写final,但是该变量依然会被编译器最终改为final的
源自JVM的内存分配问题

多线程并发的安全问题
当多个线程并发访问同一临界资源,由于线程切换时机不确定,导致多个线程操作该资源未按照
程序设计的顺序进行,导致出现错误,严重时可能出现系统瘫痪等情况
临界资源:同一时间只能被一条线程操作的资源

Thread.yield(); //static void yield()
当一个线程执行到这个方法时会主动让出本次CPU时间片并回到RUNNABLE状态

当一个方法使用关键字synchronized修饰后,这个方法称为"同步方法",多个线程不能同时在方法内部执行
将多个线程异步操作临界资源改为同步操作就可以解决多线程的并发安全问题
若在方法上直接使用synchronized,那么同步监视器对象就是当前方法所属对象this

同步块
有效的缩小同步范围可以保证并发安全的前提下尽可能的提高并发的效率
synchronized(同步监视器对象){
需要多线程同步运行的代码片段
}
同步块可以更准确的锁定需要同步运行的代码片段从而有效控制同步范围;
使用同步块时要注意,多个需要同步运行该代码片段的线程看到的同步监视器对象,即上锁的对象必须是
同一个才可以.否则没有同步效果;

静态方法若使用synchronized修饰后,那么该方法一定具有同步效果
静态方法的同步监视器对象使用的是当前类的类对象(Class的实例,类对象后面反射的课程中会介绍)
静态方法里使用synchronized块时,监视器对象是当前类名.Class// Boo.Class

互斥锁
当使用synchronized锁定多个代码片段,并且指定的同步监视器对象是同一个时,这
些代码片段间就是互斥的,多个线程不能同时在这些代码片段间一起执行

8.6
通过socket获取输出流,用于将消息发送给当前客户端//在Server端的ClientHandler的run方法里添加

pw.println(host+"说:"+message); // 将消息发送给客户端

Client start方法里//通过socket获取输入流,读取服务端发送过来的消息

str=br.readLine(); // 读取服务端发送过来的一行字符串

private PrintWriter[] allOut= {};
用来保存所有对应客户端的输出流,用于让ClientHandler之间广播消息使用
内部类是可以访问外部类的属性的,因此所有ClientHandler都可以看到它们的外部类Server
定义的属性,因此这里定义的属性可以作为它们共享数据的地方使用

将该输出流存入allOut数组中,这样每个ClientHandler实例都会做这样的事,那么所有的ClientHandler
也都可以通过这个数组获取到其他ClientHandler放进来的输出流了,从而做到共享
//1.对allOut数组扩容
allOut=Arrays.copyOf(allOut, allOut.length+1);
//2.将当前pw存入到该数组最后一个位置
allOut[allOut.length-1]=pw;


java.util.Collection 接口
集合框架
集合是用来保存一组元素的,不同的实现类实现了不同数据结构
Collection是所有集合的顶级接口,规定了所有集合都必须具备的功能
集合与数组一样,保存一组元素,但是操作元素的方法集合已经提供了

Collection下面有两个常见的子接口(分类)
java.util.List:线性表,特点是可以存放重复元素,并且有序
java.util.Set:不可以重复的集合,大部分实现类是无序的
是否为重复元素是根据元素自身equals比较的结果判定的

boolean add(E e)
向集合中添加元素,若成功添加则返回值为true
只能添加引用数据类型数据

boolean isEmpty()
判断当前集合是否为空集

int size();//返回当前集合的元素个数

c.clear();//清空集合

boolean contains(Object o)
判断集合是否包含给定元素
contains的判断依据是用给定的元素与集合每一个元素进行equals比较,只要有为true的,就认为包含,
因此元素的equals方法直接影响contains的判断结果

c.remove(p);
删除集合元素,remove方法删除元素也是依靠元素的equals方法的

集合存放元素的引用

集合操作
boolean addAll(Collection c)
将给定集合中的所有元素添加到当前集合,当调用后当前集合元素发生改变则返回true

boolean containsAll(Collection c)
判断当前集合是否包含给定集合中的所有元素

boolean removeAll(Collection c);
删除当前集合中与给定集合的共有元素,给定的集合元素不发生变化

遍历集合
集合提供了统一的遍历操作,无论哪种数据结构
实现的集合都提供了该遍历方式:迭代器模式

Iterator iterator()
Collection提供的iterator方法可以提供一个用于遍历当前集合的迭代器

java.util.Iterator接口
该接口是迭代器接口,规定了遍历集合的相关操作
所有集合都有一个用于遍历自身的迭代器实现类,我们无需关注它们的类名,以多态的角度用该接口看待并调用相关遍历方法即可

使用迭代器遍历集合的统一方式遵循为:问->取->删 (主要是问->取->问->取...)
其中删除元素不是必须操作
问 boolean hasNext()
通过迭代器查看是否还有元素可以遍历
取 E next()
通过迭代器遍历下一个元素

it.remove(); // 迭代器提供了remove方法,该方法不需要传入参数,删除的就是本次遍历通过next获取的元素
迭代器有一个要求,就是遍历的过程中不能通过集合的方法增删元素,否则遍历时会抛出异常

JDK5之后推出了一个特性:增强型for循环
也称为新循环,它不取代传统for循环的工作, 仅用来遍历集合或数组使用

for(String str:array) {
System.out.println(str);
}
新循环是编译器认可,而不是虚拟机,编译器在编译源代码时会将新循环遍历数组改为传统for循环遍历

for(Object o:c) {
String str=(String)o;
System.out.println(str);
}
使用新循环遍历集合会被编译器编译时改为使用迭代器遍历,因此要遵循迭代器使用规范,
遍历过程中不能通过集合的方法增删元素

泛型是JDK5之后推出的一个特性,又称为参数化类型,允许我们在使用一个类时指定它的属性、方法的参数和
返回值的类型,使得我们使用起来更灵活
泛型的原型是Object,不指定时就是用它
泛型在集合中广泛使用,用于规定集合中的元素类型

Collection c=new ArrayList();// 规定当前集合元素为String,添加元素时只能传入String类型元素
Iterator it=c.iterator();// 迭代器的泛型与其遍历的集合指定的泛型一致即可
while(it.hasNext()) {
String str=it.next();//不用强转,直接用String类型接
System.out.println(str);
}

java.util.List 线性表
List集合是有个可以重复的集合,并且有序,特点是提供了一组通过下标操作元素的方法
常见实现类
java.util.ArrayList
内部使用数组实现,查询性能更好,但是增删元素性能差
java.util.LinkedList
内部使用链表实现,增删元素性能好,尤其首尾增删元素性能最佳,但是查询性能差
如果对性能没有特别苛刻的要求时,通常使用ArrayList即可

List list=new ArrayList<>();//JDK7之后,右侧的<>指定泛型部分可以不再写具体的类型了
// 编译器会理解为与前面指定的一致

E get(int index) // E是指泛型,前面指定为String,则E变为String
获取指定下标处对应的元素

for(int i=0;i str=list.get(i);
System.out.println(str);
}

E set(int index,E e)
将给定元素设置到指定位置上,返回值为原位置对应的元素,所以set是替换元素操作

List提供了一对重载的add,remove方法
void add(int index,E e) //将给定元素插入到指定位置
E remove(int index) //删除并返回给定位置上的元素

List subList(int start,int end) //获取当前集合中指定范围的子集(含头不含尾)
修改子集元素就是修改原集合对应元素

list.subList(2, 9).clear(); //删除2-8位置的元素

Collection中定义了一个方法toArray,可以将当前集合转换为数组
String[] array=c.toArray(new String[c.size()]); //new String[] 长度小于c.size(),方法会自动生成一个新的长度为c.size()
// 的数组,如果大于c.size(),后面的元素为null

数组转换为List集合
数组的工具类Arrays提供了一个静态方法asList,可以将给定的数组转换为一个List集合
List list=Arrays.asList(array);

list.add("five"); // 抛出异常 java.lang.UnsupportedOperationException
由于数组是定长的,因此会改变数组元素个数的操作都是不支持的会抛出异常

List list2=new ArrayList(list);
若希望对集合元素增删操作,可以另外创建一个集合
所有的集合都支持一个参数为Collection类型的构造方法,作用是创建该集合的同时包含给定集合中的所有元素

List集合的排序
Collections.sort(list); //Collections是集合一个工具类,可以提供很多便于我们操作集合的方法
Collecitons.sort(list, new ByAge()); // 自定义排序方法
new ByAge()是一个比较器(Comparator),需要实现Comparator接口自定义排序方法

return o1.age-o2.age; //简化ByAge类中的compare方法的方法体

利用匿名内部类实现比较器
Collections.sort(list,new Comparator() {
public int compare(Person o1,Person o2) {
return o1.age-o2.age;
}
});
课后任务:实现对一组连衣裙进行自定义排序,按照价格排序,尺码排序

字符串大小比较问题
按照字符中的字符编码进行比较
其规则是按照字符中的字符编码进行比较
返回0表示相等
返回正数表示第一个字符串大
返回负数表示第一个字符串小
int n=s1.compareTo(s1); //用String类提供的compareTo方法比较,一个一个字符的编码大小比较,第一个相同则比较下一个字符...


java集合API中的Map是映射的问题
Map是一个接口,其目的是为了解决高性能查找问题。
实现Map接口的类都封装了高性能查找算法,利用Map接口的实现类就可以提高软件的查找性能,提供优秀的用户体验
Map的实现类HashMap 是最快的查找算法 没有之一!!!最常用
Map的实现类TreeMap是比较快的查找算法

key:关键信息
value:有价值的信息
Map的使用:
1.创建Map
2.将需要查询的数据,按照key-value成对存储到Map对象中,key是被检索的关键字,value是被查找到的信息
3.查询使用的时候,根据key查询对应的value

特点:
1.key不能重复!value可以重复。添加时候如果key重复就替换原有的value
2.HashMap根据key检索到value的速度非常快,和存储容量无关

put(key, value)
put方法,将被查询的数据成对的添加到map中,其中key是被检索的关键字,value是检索到的结果
将key-value成对的添加到map中,如果map中已经有key了,则替换原有的value返回被替换value。如果没有替换返回null
String val=map.put("莫言", "檀香刑");//第一次,map中没有“莫言”是添加操作 ,返回null,表示新添加
val=map.put("莫言","檀香刑,生死疲劳");//第二次,map中已经存在“莫言”,再次put方法的时候,
//是进行替换操作。返回被替换的value。key不能重复
get(key) //用于在map中根据key检索value,返回值是检索到value
如果没有检索到,返回null
由于map中允许value是null,所以检索到value为空时候,也返回null

System.nanoTime(); //纳秒,1毫秒=1000000纳秒

containsKey(key)
检查map中是否包含指定的key,如果包含返回true,不包含返回false

size() //返回key-value对的数量

isEmpty() //检查集合是否为空
如果map集合中没有数据则返回true,否则返回false

val=remove(key) //从集合中删除key对应的value,返回删除的value。如果key不存在不会产生任何结果(返回值为null)

clear() //清空集合

Map集合的遍历、迭代
import java.util.Map.Entry; //手动导入Entry类型。 Entry 条目,是一个key-value对
Set> set=map.entrySet(); //利用map.entrySet()方法将map转换为set集合,再对set进行遍历
此set集合中的元素类型是Entry
如何遍历一个map
1.导入Entry类型
2.利用entrySet()将map存储到一个set集合,其中set中每个元素是一个Entry
3.对set集合进行遍历处理,遍历时候set的元素是Entry类型
4.每个entry对象包含两个方法,getKey()获取遍历时候的key,getValue()获取遍历时候的value

hashMap==散列表==哈希表
Map中的key的类型,Value类
1.Value可以是任何引用类型
Integer、String、List、int[].....
2.key的类型,引用类型,要求必须很好的成对重写hashCode和equals
java API一般都很好地重写了这两个方法,如:String,Integer等
3.如果不很好的成对重写key的hashCode和equals方法,会造成散列表工作异常!
4.如果使用String作为key,总是没有问题的!
5.开发工具提供了自动成对重写hashCode和equals方法的功能


8.9
XML
可扩展的标记语言

为何要有XML
1.数据文件内容没有统一标准,每个厂商都不同
2.W3C组织就设计了XML统一了数据文件标准,得到了业界广泛认同!
3.XML具有可扩展性,可以支持任何数据
4.XML还具有统一的访问API,可以大大简化编程,使用方便
5.XML格式复杂,臃肿,有些场合已经使用JSON格式代替

XML基本语法
标记 tag (标签)
语法:
<标记名> 开始标记
结束标记
<标记名/> 自结束标记

例子


等价于

规则
1.标记名区别大小写
2.标记名可以任意书写,也就是标记名可以扩展
3.可以使用中文标记名,但是不建议使用中文
4.XML文件只能有唯一的一个根标记
5.标记必须配对使用,自结束标记相当于一对标记
6.标记必须合理嵌套,不能交叉嵌套
7.标记的嵌套关系可以任意扩展

内容(Content)
开始标记和结束标记中间的部分称为内容
1.可以是文本内容
2.可以是标记
3.可以是标记和文本的混合
元素
开始标记+内容+结束标记
1.根标记和全部内容称为根元素,根元素只有一个
2.元素内容中的元素称为子元素
3.没有内容的元素称为空元素
4.当前元素的上一层元素称为父元素

属性 Attribute
1.在元素的开始标记上声明属性
2.属性属于元素
3.属性的名不能重复
4.属性没有顺序
5.写法 name="value" ,其中value必须写引号
6.属性的名称和个数可以扩展
7.注意:如果有id属性,一般id的值是不同的,并且以字母为开头

实体(Entity)替换
实体相当于java的转义字符(XML没有转义字符的概念!!!用来理解而已)
特殊字符需要使用“实体”进行替换
< 替换为 <
> 替换为 >
& 替换为 &
......

CDATA
CDATA内部的特殊符号无需进行实体替换
语法:

CDATA不能嵌套使用

XML文件读取解析

什么是dom4j
1.dom4j 是第三方开源XML API
2.利用这个API可以以非常简洁的方式访问XML文件
3.dom4j底层依赖于文件流

使用Dom4j
1.导入Dom4j包
2.使用Dom4j API读取XML文件
3.利用SAXReader读取book.xml,如果读取成功就会创建一个dom对象,其结构是树形的。
如果读取失败则抛出异常:xml格式,文件找不到

Document doc=reader.read(file);
System.out.println(doc.asXML());// 检查读取结果

Maven的pom.xml配置




org.dom4j
dom4j
2.1.1

Dom4j提供了非常丰富方便的API
Element root=doc.getRootElement();//找到根元素,作为访问入口
List list=root.elements();//获取全部子元素API
for(Element e:list) {
System.out.println(e.asXML());//用新循环输出子元素
}
list=root.elements("book");//查找指定的全部子元素
Element n=e.element("name");//找到元素中的第一个符合条件的子元素
String name=n.getTextTrim();//获得元素的文本数据
String id=e.attributeValue("id");//读取元素的属性,没有元素则返回null
1.从根元素作为入口
Element root=doc.getRootElement();
2.搜索全部子元素,获得元素的全部子元素
List list=element.elements();
3.搜索指定名字的全部子元素,获得一个元素列表
List list=element.elements("元素名");
4.搜索第一个指定名字的子元素,一般用在子元素名唯一的时候,当子元素不存在的时候,返回null
Element e=element.element("元素名");
5.获取元素中的文本
String str=element.getText();
String str=element.getTextTrim(); //去除空白,常用!
6.可以直接获取子元素中的文本
String str=element.elementTextTrim("子元素名");
等价于
String str=element.element("子元素名").getTextTrim();
7.获取元素中的属性
String value=element.attributeValue("属性名");

回顾:
1.XML语法:注释、标记(标签)、内容、元素、属性、实体、CDATA
2.XML解析:Dom4j、W3c DOM、SAX、Jdom、XML Stream
-编码最便捷的是Dom4j 网站 https://dom4j.github.io/

 

WebServer是模拟TomCat的一个web容器
web容器可以同时管理多个网络应用,并且提供了与客户端(通常是浏览器)的网络连接以及传输数据
和与客户端的应用层交互(涉及到TCP协议以及HTTP协议)上面的支持。有了web容器,使得程序员
更多的精力是放在具体web应用的业务上
webapp(网络应用):它包含的内容大致有网页,图片,其它静态素材以及java程序代码,就是我们上网
时俗称的一个“网站”的全部内容

TCP协议,处于传输层,负责两台计算机之间通过网络传输数据的协议
HTTP协议,处于应用层,规定了双方发送数据的格式,以及交互规则

URL 统一资源定位
http:// doc.tedu.cn /maven/index.html
协议名称 计算机地址信息(host) 抽象路径

ClientHandler线程
该线程负责处理与指定客户端的交互工作,处理过程分为三步:
1:准备工作
2:处理请求
3:发送响应

WebServer start() 启动一个线程处理该客户端交互

测试读取一行字符串的操作
客户端连接后会主动发送一个HTTP请求(request),而一个请求中的请求行和消息头部分有一个
特点,都是以行为单位的字符串,结尾为CRLF。因此我们在做解析请求之前,先测试一个功能,读取
一行字符串
实现:
1:添加ClientHandler类,与聊天室一样,当一个客户端连接后启动一个线程处理该客户端(这样
才能支持同时多个客户端的连接)
2:ClientHandler中通过socket获取输入流后,测试读取一行字符串并将其输出到控制台

开始解析请求
一个HTTP的请求包含三部分,请求行,消息头,消息正文
因此我们设计一个类HttpRequest,用这个类的每一个实例表示客户端发送过来的一个具体请求内容
所有请求的部分都设计对应的属性来保存对应值
实现:
1.创建一个包:com.webserver.http
这个包保存所有与HTTP协议有关的类
2.在http包中定义类:HttpRequest
定义属性对应请求中各项内容
3.定义构造方法用于初始化HttpRequest
初始化的过程就是读取客户端发送的请求内容并进行解析的过程
4.在ClientHandler的第一步“准备工作”里实例化HttpRequest
完成请求的解析

HttpRequest类
请求对象
该类的每一个实例用于表示客户端发送过来的一个具体的请求内容
一个请求由三部分组成:请求行,消息头,消息正文

三个String变量保存//请求行相关信息
用Map保存//消息头相关信息
构造方法,用于初始化请求对象//消息正文相关信息
定义三个方法 解析请求行 解析消息头 解析消息正文

parseHeaders方法
循环调用readLine方法读取每一行字符串,每一行就是一个消息头,如果readLine方法
返回的字符串时一个空字符串时就应当停止循环读取操作了(因为单独读取到了CRLF)
读取到每一个消息头后,我们可以按照": "(即:冒号空格)来进行拆分,将消息头的名字
作为key,消息头的值作为value保存到header这个Map中完成消息头的解析工作

ClientHander应当根据request对象中用户请求的资源的抽象路径(uri属性的值)来判断该资源是否存在
如果存在,我们将在下一步响应客户端时将该资源回复.

因此,我们要先准备一些必要的资源,如页面:
首先我们在项目目录下新建一个名为webapps的目录用于保存我们这个web容器下每一个具体的网络应用,
每个网络应用以一个子目录的形式保存,目录名就是这个网络应用的名字,然后将该网络应用下的内容存于
该目录里(页面/图片等等)

实现:
1.在当前项目目录下新建目录:webapps用于保存所有的网络应用
2.在webapps目录下新建一个网络应用(一个目录),起名为myweb
3.在myweb目录下新建一个页面index.html
4.在ClientHandler中完成第二步处理请求,首先通过request获取抽象路径,然后从webapps
目录下根据该抽象路径寻找资源并定义分支判断然后分别打桩

完成后,在另一个分支中(资源不存在)完成响应客户端404页面的操作
实现:
1.在webapps目录下新建一个子目录root
2.在root目录下新建一个404.html页面,不创建在myweb目录下是因为无论哪个应用只要
用户请求的资源不存在都要响应404,所以这个页面应当是所有网络应用公用的页面
3.在404.html中居中显示一行字:
404,资源不存在!
4.在ClientHandler分支中没有该资源,响应404页面给客户端
其中状态代码为404,状态描述为NOT FOUND
响应头还是Content-Type与Content-Length
响应正文为: ./webapps/root/404.html页面内容

v6 本版本重构响应
上个版本中,我们在ClientHandler中处理请求后的分支中都是直接将发送响应的具体细节写在这里
这里造成了代码的重复,因此本版本我们先要重用发送响应的代码
与请求设计思路一致,在定义一个类:HttpResponse,用它的每一个实例表示一个具体的响应对象,
设置要给客户端发送的具体信息后将响应对象以一个标准的响应格式发送给客户端

响应对象
该类的每一个实例用于表示发送给客户端的一个具体的响应内容
每个响应包含三部分内容:状态行,响应头,响应正文

页面上在使用路径指定某个资源时,我们通常也可以使用相对路径,
而在页面中"./"当前目录会被浏览器理解为当前页面所在的目录。
比如,我们访问此页面路径为:
http://localhost:8088/myweb/index.html
那么浏览器认为"./"是:
http://localhost:8088/myweb/
因此下面的图片指定路径"./logo.png",那么它的实际路径就是:
http://localhost:8088/myweb/logo.png

本版本继续重构响应

上个版本中,我们已经将响应以HttpRequest对象形式表示并利用flush发送给客户端。并且在主类
WebServer中循环接收客户端的连接并进行了处理

本版本导入了doc.tedu.cn网站上提供的学子商城项目后发现问题:页面无法正常响应所有资源,原因在于
无论浏览器请求页面中的任何资源(css,js,png等)我们服务端在响应该资源时在响应头Content-Type指明
资源类型时都是发送固定的text/html,这使得浏览器无法正确理解其请求的资源出现无法正确显示的情况

本版本先解决HttpResponse发送响应头时固定发送两个头:Content-Type和Content-Length,改为
根据设置的响应头进行发送

实现思路:
在HttpReponse中添加一个Map类型的属性,用于保存所有要发送给客户端的响应头
然后在发送响应头的方法中遍历这个Map将所有的响应头发送给客户端
这样,将来在发送前,我们可以通过处理结果来向相应对象中设置要发送的响应头来进行发送

本版本继续重构响应

本版本要根据实际客户端请求的资源响应对应的类型来达到让客户端能
正确显示出学子商场页面及以后所有的页面内容

常见的资源类型与Content-Type值的对应关系
html text/html
css text/css
png image/png
jpg image/jpeg
gif image/gif
js application/javascript

本版本继续重构响应

上个版本中我们在ClientHandler处理请求的过程中创建了一个Map保存了所有的资源后缀
和对应的Content-Type的值,并且根据资源的后缀获取到对应的该值的信息来设置响应头
从而实现了正常显示学子商场这种有较多资源的页面
但是,这种实现方法会导致每次请求一个资源都要创建这个Map,而这个Map实际要有1000
多种资源对应并且内容是固定的,因此我们不应当每次处理时都创建一份,将其定义为静态内容存
一份即可

实现:
1.在com.webserver.http包中定义一个类:HttpContext,使用这个类保存所有HTTP协议规定的内容
2.在HttpContext中定义常量:static Map mimeMapping
3.完成相关初始化工作并对外提供get方法,可以根据后缀名获取对应的Content-Type的值
解析conf/web.xml文件
将根标签下所有名为的子标签获取出来,并将它下面的:
标签中的文本作为key
标签中的文本作为value
初始化mimeMapping这个Map,初始化完毕后,mimeMapping这个Map中应当有1000多个元素

引入tomcat整理的所有资源类型与Content-Type对应值的xml文件用于初始化mimeMapping。
在项目目录下新建conf,并导入web.xml文件
然后在HttpContext的初始化方法initMimeMapping中通过解析该xml文件完成初始化

现在ClientHandler还存在一个小问题,当我们设置响应对象时,设置响应正文的同时还要设置
两个用于说明正文长度和类型的Content-Length,Content-Type,我们完全可以将设置两个响应头的工作
放到设置响应正文的方法中,这样我们在设置响应正文时就不用每次都为其添加这两个响应头了

本版本开始处理业务

以用户注册操作为例,来实现。

正常的注册流程:
1.用户请求注册页面
2.在注册页面输入注册信息并点击注册按钮
3.数据提交到服务端
4.服务端根据提交的数据保存该注册信息
5.服务端响应注册结果页面给客户端

实现:
1.在webapps/myweb目录下新建一个页面reg.html并定义表单,提交路径
action="./reg" method="get"并在表单中定义注册信息对应的输入框
2.当该表单提交后,我们发现,get方式提交表单时所有输入框中的值是拼接带URL
中的,以"?"隔开每个参数再以"&"分割传递。因此我们在解析请求时要针对请求行中的
抽象路径部分进一步解析。
首先在HttpRequest类中添加三个新的属性
String requestURI:记录uri中请求部分(?左侧内容)
String queryString:记录参数部分(?右侧内容)
Map parameter:保存参数部分中的每一个参数

首先在解析之前,要先判断当前uri是否含有参数,判断的依据是看uri当中是否含有"?",有则说明含有参数,否则就是没有参数
如果没有参数,那么直接将uri的值赋值给requestURI即可;
若有参数,则应当先按照"?"将uri拆分然后将"?"左侧内容赋值给requestURI,将"?"右侧内容赋值给queryString;
接着将参数部分在按照"&"拆分为每一组参数,每组参数再按照"="拆分为参数名与参数值,并将名字作为key,值作为value
保存到parameter这个Map中完成解析;

上一个版本中,我们已经完成了在HttpRequest中解析客户端提交数据的操作了。本版本开始来
根据用户的请求及提交的数据完成对应的业务处理
1.通过request获取用户提交的注册信息
2.将信息写入user.dat文件保存
3.设置response响应注册结果页面

实现
1.首先创建一个新的包:com.webserver.servlet
2.在这个包中定义用于处理注册的类:RegServlet
3.在RegServlet中定义service方法用于处理业务
4.ClientHandler在处理业务的分支中实例化RegServlet并调用其service方法处理注册业务
5.在webapps/myweb目录下新建一个页面reg_succecc.html的注册信息,该页面用于提示
用户注册成功。
6.service方法中首先通过request获取用户表单提交的注册信息,然后打开RandomAccessFile,
将该条记录写入user.dat文件中,写入后设置response对象响应注册成功页面

本版本补全注册业务操作

本版本将注册中验证用户是否已经存在的工作完成
实际上网中,通常注册时用户名是不允许重复的,因此我们也要做该验证

实现:
1.首先在webapps/myweb目录下新建一个页面:
hava_user.html,该页面提示一行字:该用户已存在,请重新注册
并在下方添加一个超链接点击后返回到注册页面
2.在RegServlet的service方法中,当通过request获取到用户注册信息(用户名,密码
昵称,年龄)并创建RandomAccessFile后:
首先应当循环读取user.dat文件中现有的每条记录然后比对每条记录的用户名是否是
当前提交的注册用户的名字,若是则说明该用户已存在,直接设置response响应
have_user.html即可
若读取完user.dat文件并且不存在重名的,则执行原有的逻辑将该记录写入user.dat文件

本版本解决地址栏传递中文的问题

上个版本中我们完成了用户注册功能,但是如果输入框中输入了中文字符,会出现地址栏
提交数据如:/myweb/reg?username=%E8%8C%83%xxx=xxx.....
而不是直接看到:/myweb/reg?username=范=。。。

这是因为抽象路径部分是包含在请求的请求行当中的,而HTTP协议要求请求的文本部分
只能是ISO8859字符集中的内容(欧洲字符集,不支持中文)

因此解决办法为:
首先将中文这样的字符按照指定编码转换为字节,2进制的内容是由1,0两个数字,这两个
是ISO8859中支持的字符,但是一个字节就由8位2进制组成,内容又太长,因此我们再将
2进制每个字节的8位改为用2位16进制表示。为了和实际的英文数字区分开,16进制的
内容前面用"%"标识
因此有了"%XX"这样表示1字节的内容了。
例如:
"范"用UTF-8转换为2进制后内容为:
11101000 10001100 10000011
用16进制表示后内容为:
%E8%8C%83

queryString=URLDecoder.decode(queryString, "UTF-8");//对queryString中包含的%XX进行转码

本版本解决空请求问题

HTTP协议中对此有说明,允许客户端发送空请求。即:客户端连接服务端后
没有发送一个标准的HTTP请求过来。
但是这样导致的后果是我们在解析请求时读取第一行字符串后解析请求行就会出现
拆分得到数据下标越界。并且客户端发送空请求这个操作完毕后就会与服务端
断开连接,如果按照我们正常处理的流程,最终要发送响应给客户端,测试还会
出现Socket异常

思路:
在解析请求行时若发现本次请求是空请求时,我们就对外抛出一个空请求异常(自
定义的异常),然后再通过HttpRequest的构造方法将该异常抛出给ClientHandler,
使得它可以忽略后续所有处理工作

此版本支持post请求提交数据

注册页面中我们将form表单的提交方式改为:post
此时再次提交注册信息会发现用户输入的信息不再
显示在URL当中,而是会被包含在请求的消息正文
部分.因此我们在HttpRequest要请求中提供的消息
正文内容才可以得到.

完成用户登录操作

用户请求登录页面,然后页面上输入用户名和密码,点击登录按钮后提交登录操作。之后
看到登录成功或失败的页面

实现:
1.在webapps/myweb目录下准备对应的页面
1.1 login.html,登录页面表单action指定提交路径:action="./login"
1.2 login_success.html,登录成功提示页面。显示一行字:登录成功,欢迎回来
1.3 login_fail.html,登录失败提示页面,显示一行字:登录失败,用户名或密码不正确

2.在com.webserver.servlet中添加用于处理登录业务的类:LoginServlet,并定义service方法

3.修改ClientHandler,在第二步处理请求的环节,当判断path的值不是注册业务下面添加一个
新的分支,判断是否为登录的值:/myweb/login,如果是,则实例化LoginServlet并调用
其service方法处理登录

4.完成service方法的逻辑
4.1:首先通过request获取用户登录页面上表单提交的登录信息(用户名和密码)
4.2:使用RandomAccessFile读取user.dat文件并逐条比对用户名和密码,如果比对成功
则设置response响应登录成功,如果密码有误或者user.dat文件中没有该用户则响应登录失败页面

重构操作

我们定义一个HttpServlet,作为所有Servlet的超类在这里定义一个抽象方法:service,这样
就要求所有的子类必须要有这个方法来处理业务了。
然后再将一些重复的代码提取到这里定义为方法,使所有子类都可以复用,比如转跳页面的操作

java反射机制
java反射机制是一个动态的机制,允许我们在程序的运行过程中通过字符串来指挥程序实例化,操作属性,调用方法等。
这使得代码提高了灵活度,但是反射机制会带来更多的资源开销和较慢的运行效率(相较于硬编码)。
程序不应当过于依赖反射,它应当只是起到画龙点睛的作用,在合适的时候使用

Class类
Class类称为类对象,它的每一个实例表示JVM加载的一个类,并且JVM内部每个被加载的类都有且只有唯一一个Class实例与之对应
通过类对象我们可以得到其表示的类的一切信息,便于我们在程序运行期间来通过它操作其表示的类

获取一个类的类对象有三种方式:
1.调用该类的静态属性class,每个类都有,获取的就是该类的类对象,基本类型也有
2.通过Class的静态方法forName,该方法可以指定要获取的类的名字从而得到该类的类对象
3.通过类加载器ClassLoader得到

Class获取其表示的类的方法有两种
getMethods():获取这个类所有方法(包含继承的)
getDeclaredMethods():仅获取本类自己定义的

通过反射机制进行实例化对象
1.加载要实例化的类的类对象
2.通过类对象的newInstance()方法实例化
newInstance方法时调用无参构造器实例化的,因此要求Class表示的类必须有无参构造器才可以

通过反射机制调用方法
1、加载要操作的类的类对象
2、利用类对象实例化其表示的类的实例
3、通过类对象获取要调用的方法
4、调用该方法

Method method2=cls.getMethod("sayHello", String.class,int.class);//调用有参

JDK5之后推出了一个特性:变长参数
变长参数的实际类型为数组,而且变长参数只能是当前方法的最后一个参数,且只能定义一个
public static void test(int a,String... s) {
System.out.println(s.length);
}

//强制访问该方法
method.setAccessible(true);
method.invoke(o);//反射就可以调用私有方法了

解析conf/servlets.xml文件
将根标签下所有的标签获取到并且将每个标签中的属性:path的值作为key
className的值利用反射加载对应的类并实例化,将实例化的对象作为value存入到servletMapping完成初始化

线程池
线程池是管理线程的一套解决方案,主要作用:
1.控制线程数量
线程数量过多会导致过多的资源消耗,并且会导致CPU过度切换降低整体并发性能
2.重用线程
线程不应当随着任务的生命周期一致,频繁的创建和销毁线程会给系统带来额外的开销

ExecutorService threadPool=Executors.newFixedThreadPool(3);//创建一个固定大小的线程池
threadPool.execute(r);//执行线程
threadPool.shutdown();//停止线程池,等待目前的任务做完就停止
threadPool.shutdownNow();//立即停止线程池,不等待目前的任务做完就停止


java.util.Date
Date的每一个实例用于表示一个确切的时间(精度为毫秒)
内部维护一个long值,该值表示的自1970年1月1日00:00:00到当前Date表示的时间之间经过的毫秒
Date存在时区等设计缺陷,因此大部分方法都被声明为过时的在以后的开发中不应当再使用

long getTime()
获取Date内部维护的毫秒值

java.text.SimpleDateFormat
该类可以将Date和String之间按照指定的格式进行相互转换

SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//按指定格式 yyyy-MM-dd HH:mm:ss
String format(Date date)
将给定的Date对象按照当前SimpleDateFormat指定的日期格式转换为字符串

Date date = sdf.parse(str);//将一个字符串解析为Date对象

java.util.Calendar 日历类
Calendar calendar = Calendar.getInstance();
Calendar是一个抽象类,规定了日历类操作时间的一系列方法
可以使用其提供的静态方法:getInstance来获取一个当前系统所在地区适用的实现类,大部分地区获取
回来的是GregorianCalendar,即:阳历实现类

Calendar提供的方法:
Date getTime()
可以将当前Calendar表示的日期以一个Date实例形式返回

void setTime(Date date)
调整当前Calendar使其表示给定的Date所表示的日期

int get(int field)
获取当前Calendar表示的日期中指定时间分量所对应的值不同的时间分量用不同的整数表示,无需记忆,Calendar提供了大量的常量与之对应
Calendar calendar=Calendar.getInstance();
int year = calendar.get(Calendar.YEAR);
int month=calendar.get(Calendar.MONTH)+1;//月需要注意从0开始,即:0表示1月,1表示2月
int day=calendar.get(Calendar.DAY_OF_MONTH);
和"天"相关的时间分量有:
DAY_OF_MONTH:月中的天
DAY_OF_WEEK:周中的天
DAY_OF_YEAR:年中的天
DATE:月中的天,与DAY_OF_MONTH一致
int h=calendar.get(Calendar.HOUR_OF_DAY);//24小时制的时
int m=calendar.get(Calendar.MINUTE);
int s=calendar.get(Calendar.SECOND);
//按照美国的习惯,一周的第一天是周日
days=calendar.get(Calendar.DAY_OF_WEEK)-1;
String[] date= {"日","一","二","三","四","五","六"};
System.out.println("今天是周"+date[days]);
int d=calendar.getActualMaximum(Calendar.DAY_OF_YEAR);//获取指定时间分量所允许的最大值


void set(int field,int value)
调整当前Calendar指定时间分量为给定的值
//调整年为2008
calendar.set(Calendar.YEAR, 2008);
//调整月为8月
calendar.set(Calendar.MONTH, Calendar.AUGUST);
//调整为8号
calendar.set(Calendar.DATE, 8);

void add(int field,int amount)
对指定的时间分量加上给定的值,若给定的值为负数则是减去

//查看3年2个月零25天以后那周的周三是哪天?
calendar.add(Calendar.YEAR, 3);
calendar.add(Calendar.MONTH, 2);
calendar.add(Calendar.DAY_OF_YEAR, 25);
calendar.set(Calendar.DAY_OF_WEEK, Calendar.WEDNESDAY);


lambda表达式 JDK8之后推出的一个特性
lambda可以用更简短的方式创建匿名内部类。该语法使得我们可以以"函数式编程"
lambda创建匿名内部类时实现的接口必须只能有一个抽象方法,否则不可以使用

语法:
(参数列表)->{
方法体
}
Runnable r2=()->{
System.out.println("hello");
};
//如果只有一行代码,{}可以省略
Runnable r3=()->System.out.println("hello");

//方法含有参数时使用lambda,lambda中参数是不需要指定类型的,编译器会根据代码分析出类型
Comparator c=(o1,o2)->{
return o1.length()-o2.length();
};

//如果方法最终使用return关键字进行返回,那么当可以忽略"{}"时,return关键字也要一同忽略
Comparator c=(o1,o2)->o1.length()-o2.length();

你可能感兴趣的:(Java 常用API)