API基础第一天:

笔记:

  1. String:字符串类型

    • java.lang.String使用的final修饰,不能被继承
    • 字符串底层封装了字符数组以及针对字符数组的操作算法
    • Java字符串在内存中采用Unicode编码方式,任何一个字符对应两个字节的编码
    • 字符串一旦创建,对象内容永远无法改变,但字符串引用可以重新赋值
  2. 常量池:

    • java对字符串有一个优化措施:字符串常量池(堆中)
    • java推荐我们使用字面量/直接量的方式来创建字符串,并且会缓存所有以字面量形式创建的字符串对象到常量池中,当使用相同字面量再次创建字符串时会重用对象以减少内存开销,避免内存中堆积大量内容相同的字符串对象
    /*使用字面量创建字符串时:1.JVM会检查常量池中是否有该对象:1)若没有,则创建该字符串对象并存入常量池2)若有,则直接将该对象返回而不再创建一个新的字符串对象*/
    /*
    String s1 = "123abc"; //常量池还没有,因此创建该字符串对象,并存入常量池
    String s2 = "123abc"; //常量池已有了,直接重用对象
    String s3 = "123abc"; //常量池已有了,直接重用对象
    //引用类型==,比较地址是否相同
    System.out.println(s1==s2); //true
    System.out.println(s1==s3); //true
    System.out.println(s2==s3); //true
    *//*常见面试题:String s = new String("hello");问:创建了几个对象?答:2个第一个:字面量"hello"---java会创建一个String对象表示字面量"hello",并将其存入常量池第二个:new String()---new String()时会再创建一个字符串,并引用hello字符串的内容*/
    /*
    String s1 = new String("hello"); //s1装的是new String()对象的地址
    String s2 = "hello"; //s2装的是字面量"hello"的地址
    System.out.println("s1:"+s1); //hello
    System.out.println("s2:"+s2); //hello
    System.out.println(s1==s2); //false,因为s1与s2的地址不同//字符串实际开发中比较相等的需求都是比较字符串的内容
    //因此我们应该使用字符串提供的equals()方法来比较两个字符串的内容
    System.out.println(s1.equals(s2)); //true,因为s1与s2的内容相同
    *//*
    String s1 = "123abc";
    String s2 = "123abc";
    System.out.println(s1==s2); //true,s1与s2地址相同s1 = s1+"!"; //创建新对象并把地址赋值给s1
    System.out.println(s1==s2); //false,s1为新的对象的地址,与s2不同了
    *///如下代码:常量池中会有3个存储,一个是123abc的地址,一个是123的地址,一个是abc的地址
    //一个新的对象,它的值也是123abc
    String s1 = "123abc";
    //编译器在编译时,若发现一个计算表达式可以在编译期间确定结果,
    //则直接运算好并将结果保存到表达式中 相当于String s2 = "123abc";
    String s2 = "123"+"abc";
    System.out.println(s1==s2); //true,s1与s2共用常量池中的String s3 = "123";
    //当字符串拼接产生的内容与常量池是某内容相同时,也不会重用常量池的对象
    String s4 = s3+"abc"; //创建一个新的对象存储123abc
    System.out.println(s4==s1); //false
    
  3. String常用方法:

    • length():获取字符串的长度(字符个数)

      String str = "我爱Java!";
      int len = str.length(); //获取str的长度
      System.out.println(len); //7
      
    • trim():去除当前字符串两边的空白字符

      String str = "  hello world            ";
      System.out.println(str); //  hello world
      str = str.trim(); //去除当前字符串两边的空白字符
      System.out.println(str); //hello world
      
    • indexOf(String str):检索给定字符串在当前字符串的开始位置

      int lastIndexOf(String str):
      检索给定字符串在当前字符串中最后一次出现的位置

      //            0123456789012345
      String str = "thinking in java";
      int index = str.indexOf("in"); //检索in在字符串str中出现的开始位置
      System.out.println(index); //2index = str.indexOf("IN"); //当前字符串不包含给定内容IN,所以返回-1
      System.out.println(index); //-1index = str.indexOf("in",3); //从第4个字符开始找in第一次出现的位置
      System.out.println(index); //5index = str.lastIndexOf("in"); //找in最后一次出现的位置
      System.out.println(index); //9
      
    • substring(int start,int end):截取当前字符串中指定范围内的字符串(含头不含尾–包含start,但不包含end)

      public class SubstringDemo {public static void main(String[] args) {/*//            01234567890String str = "www.tedu.cn";String name = str.substring(4,8); //截取第4个到第7个----下标System.out.println(name); //teduname = str.substring(4); //从第4个一直截取到字符串末尾----下标System.out.println(name); //tedu.cn*/String name = getName("www.tedu.com.cn");System.out.println(name); //teduString str = getName("http://www.google.com");System.out.println(str); //google}/*** 获取给定网址中的域名* @param line 网址* @return 返回域名*/public static String getName(String line){//012345678901234//www.tedu.com.cn  第一个点到第二个点之间的字符串int start = line.indexOf(".")+1; //4,加1目的是为了找到点后的第一个字符的位置int end = line.indexOf(".",start); //8,从start往后找第一个.的位置return line.substring(start,end);}
      }
      
    • charAt():返回当前字符串指定位置上的字符

      //            0123456789012345
      String str = "thinking in java";
      char c = str.charAt(9); //获取位置9所对应的字符
      System.out.println(c); //i
      
    • startsWith(String str)和endsWith(String str):判断当前字符串是否是以给定的字符串开始/结尾的

      String str = "thinking in java";
      boolean starts = str.startsWith("think"); //判断str是否是以think开头的
      System.out.println("starts:"+starts); //trueboolean ends = str.endsWith(".png"); //判断str是否是以.png结尾的
      System.out.println("ends:"+ends); //false
      
    • toUpperCase()和toLowerCase():将当前字符串中的英文部分转为全大写/全小写

      String str = "我爱Java!";
      String upper = str.toUpperCase(); //将str中英文部分转为全大写
      System.out.println(upper); //我爱JAVA!String lower = str.toLowerCase(); //将str中英文部分转为全小写
      System.out.println(lower); //我爱java!
      
    • valueOf():String类中提供的静态方法,将其它数据类型转换为String

      int a = 123;
      String s1 = String.valueOf(a); //将int型变量a转换为String类型并赋值给s1
      System.out.println("s1:"+s1); //123double dou = 123.456;
      String s2 = String.valueOf(dou); //将double型变量dou转换为String类型并赋值给s2
      System.out.println("s2:"+s2); //123.456String s3 = a + ""; //任何内容与字符串连接结果都是字符串,效率低(下周一才能体会)
      System.out.println(s3); //123
      

补充:

  1. ASCII:美国标准编码,是美国最早的字符集,也是计算机最底层的字符集,一个字节
  2. GBK:国标编码,中国自己的编码,总共6万多个
  3. Unicode:万国码,装全世界所有符号
  4. UTF:在Unicode基础之上做的二次编码,里面加入了一个长度信息来标记是按一个字符解析还是按两个字符算

结论:互联网上真正使用的并不是unicode,真正传输出的是UTF这种带长度信息的编码,拿到UTF数据后再把长度去掉,还原成unicode编码。

Java API基础第二天

StringBuilder

String 类型的连接性能不好,Java提供了StringBuilder解决字符串连接性能问题。

简单理解 StringBuilder性能好!(重点!)

String s1 = "ABC";
String s2 = "def";
String s3 = s1 + s2;

字符串连接性能测试:

String str = "";
long t1 = System.currentTimeMillis();
for(int i=0; i<50000; i++){str = str + "A";
}
long t2 = System.currentTimeMillis();
System.out.pritnln(t2 - t1);

StringBuilder 用于提升String字符串的连接性

  • StringBuilder称为可变字符串
  • StringBuilder内部也是字符数组, 其API可以直接修改其内部数组的内容
  • 当数组容量不足时候, 会自动扩容
  • 运算期间会尽力减少创建数组的数量。
package string;public class StringBuilderDemo03 {public static void main(String[] args) {/** 测试StringBuilder的连接性能*/StringBuilder buf = new StringBuilder();long t1 = System.currentTimeMillis();for (int i =0; i<50000; i++){buf.append("A");}long t2 = System.currentTimeMillis();System.out.println(t2-t1);}
}

StringBuilder API

buf-> char[]{A, C, B, 0, 0, 0, 0, 0, 0, 0, 0...0}
//           0  1  2  3  4  5
StringBuilder buf = new StringBuilder();
buf.append("A") .append("A") .append("A").append("B") .insert(1,"C").delete(2,4);
Stirng str = buf.toString();
  • append() 追加, 在StringBuilder的后面添加字符,当容量满了,会自动扩容, 扩容规则 1倍+2;
  • insert(位置,字符) 插入字符;
  • delete(开始位置, 结束位置): 删除一定范围的字符,包括开始,不包括结束
  • StringBuilder的API返回的大多是当前对象,可以连续使用.调用方法。
  • toString() 方法可以讲StringBuilder转换为String

正则表达式

用于检测、测试字符串规则的表达式.

经常用于检测字符串是否符合特定的规则,在网站上经常用于检测用户输入数据是否符合规范:

  • 检测 用户名 是否为 8~10 数字 英文(大小写)
  • 检测 电话号码是否符合规则
  • 检测 邮箱地址是否符合规则

正则HelloWorld

最简单的正则表达式:“HelloWorld” 表示

  • 一共有10个字符

  • 出现的顺序必须是 HelloWorld

  • Java 提供了正则API, 用于检测一个字符串是否符合,正则规则

    • boolean matchs(正则) 检测当前字符串是否符合正则规则
正则规则 rule = "HelloWorld"
字符串: s1 = "HelloKitty";
字符串: s2 = "HelloWorld";
// s1 s2 中那个字符串符合 rule 约定的规则?
boolean b1 = s1.matches(rule); //false
boolean b2 = s2.matches(rule); //true
package string;public class RegDemo05 {public static void main(String[] args) {/** 测试正则表达式*///定义正则表达式String rule = "HelloWorld";//定义被检测的字符串String s1 = "HelloKitty";String s2 = "HelloWorld";//检测 s1 是否符合规则boolean b1 = s1.matches(rule);//检测 s2 是否符合规则boolean b2 = s2.matches(rule);System.out.println(b1);System.out.println(b2);}
}

字符集

匹配一个有效字符范围。

语法:

[123456]

意义:

  • 匹配一个字符
  • 其有效范围: 1 2 3 4 5 6 中的某一个

正则规则例子:

Hello[123456]
  • 匹配6个字符
  • 前5个必须是Hello
  • 第6个字符,必须 1 2 3 4 5 6 中的一个

如, 可以匹配的字符串:

  • “Hello1”
  • “Hello2”
  • “Hello3”
  • “Hello6”
  • “Hello7” 不可以匹配!
  • “HelloA” 不可以

正则例子: 我[草去艹]

字符范围

规则 正则表达式 范围
匹配 0~9 一个字符 [0123456789] [0-9]
匹配A-Z一个字符 [ABCDEFGHIJKLMNOPQRSTUVWXYZ] [A-Z]
匹配a-z一个字符 [a-z]
匹配a-zA-Z一个字符 [a-zA-Z]

栗子:

Hello[1-6]

预定义字符集

规则 正则 预定义字符集 栗子
匹配一个数字 [0-9] \d Hello\d
匹配一个单词字符 [a-zA-Z0-9_] \w A\w
匹配一个空白字符 \s Hello\sWorld
匹配任意一个字符 . A.
匹配一个非数字 \D
匹配一个非空白 \S
匹配一个非单词字符 \W

栗子, 网站上规则 用户名规则是6个单词字符:

正则规则: \w\w\w\w\w\w

java String: "\\w\\w\\w\\w\\w\\w"

测试案例:

package string;public class RegDemo07 {public static void main(String[] args) {/** 测试 用户名规则:6个单词字符组成* - \ 在java字符串中需要进行转义为 \\*///正则表达式:  String reg = "\\w\\w\\w\\w\\w\\w";System.out.println(reg);//被检查的字符串String s1 = "Jerry1"; //可以通过检查String s2 = "Tom-12"; //不可以通过检查String s3 = "Andy";   //不可以通过检查System.out.println(s1.matches(reg));System.out.println(s2.matches(reg));System.out.println(s3.matches(reg));}
}

数量词

约定左侧元素出现的次数。

栗子:

\w\w\w\w\w\w  等价  \w{6}

语法:

X{n}  规定左侧X出现n次
X{n,m} 规定左侧X出现最少n次, 最多m次
X{0,n} 规定左侧X出现0到n次
X{n,}  规定左侧X出现最少n次
X?   和 X{0,1} 等价,X可以没有或者有一个
X+   和 X{1,} 等价,X至少有一个,多了随意,简称:一个以上
X*   和 X{0,} 等价,X至少有0个,多了随意 简称:0个以上

栗子:

  • 网站的用户名是 8~16个单词字符: \w{8,16}
  • 网站的密码是单词字符, 最少8个, 多了不限: \w{8,}
  • 匹配Hello World,中间至少有一个空白: Hello\s+World
    • 不能匹配 : “HelloWorld”
    • 不能匹配: “Hello World!”
    • 能匹配: “Hello World”
    • 能匹配: “Hello World”
    • 能匹配: “Hello World”

特殊字符转义

如何匹配字符 [ ] ? + * . , 使用 \特殊字符, 进行转义!

\. 匹配点
\[ 匹配 [
\? 匹配 ?
\* 匹配 *
\+ 匹配 +
\\ 匹配 \
...

如下正则的意义:匹配 www.tedu.cn 域名

  • www.tedu.cn 匹配:

  • www.tedu.cn 通过

  • wwwAteduAcn 通过

  • www-tedu-cn 通过

  • www\.tedu\.cn 匹配

    • www.tedu.cn 通过
    • wwwAteduAcn 不通过
    • www-tedu-cn 不通过

案例:如何检查一个字符串是否为正确的IPv4地址

正确IP:

“192.168.1.25” “192.168.199.1” “10.0.0.20” “8.8.8.8”

错误的IP:

“10-10-10-20” “192点168点5点25”

正则:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}

分组

讲一组规则作为整体进行处理

栗子正则:

  1. \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}

  2. (\d{1,3}\.)(\d{1,3}\.)(\d{1,3}\.)\d{1,3}

  3. (\d{1,3}\.){3}\d{1,3}

package string;public class RegDemo11 {public static void main(String[] args) {/** 检查IP地址是否符合规则*///定义正则规则//String reg = "\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}";String reg = "\\d{1,3}(\\.\\d{1,3})(\\.\\d{1,3})(\\.\\d{1,3})";//String reg = "(\\d{1,3}\\.){3}\\d{1,3}"; //测试分组//定义被检查的字符串String ip1 = "192.168.2.70";String ip2 = "10.0.0.20";String ip3 = "8.8.8.8";//定义错误的被检查字符串String ip4 = "192点168点2点70";String ip5 = "192-168-2-70";//检查System.out.println(ip1.matches(reg));System.out.println(ip2.matches(reg));System.out.println(ip3.matches(reg));System.out.println(ip4.matches(reg));System.out.println(ip5.matches(reg));}
}

栗子2:

  1. \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}

  2. \d{1,3}(\.\d{1,3})(\.\d{1,3})(\.\d{1,3})

  3. \d{1,3}(\.\d{1,3}){3}

区别:

(\d{1,3}\.){3}\d{1,3}  (分组){3} 分组的整体出现3次
\d{1,3}\.{3}\d{1,3}   \.{3} .必须出现2次,可以匹配 “192...168”

java 正则API

  • matches 检查字符串是否整体符合正则表达式规则
  • split 劈开
  • replaceAll 全部替换

Split 劈开字符串(重要)

将一个字符串劈开为几个子字符串:

  • “192.168.5.140” 劈开为 “192” “168” “5” “140”
  • “1, Tom, 110, tom@tedu.cn” 劈开为 “1” “Tom” “110” “tom@tedu.cn”

使用:

str 存储的是被劈开的字符串
正则 用于匹配劈开的位置点, 如: , 或者 \.
返回值 是劈开以后的数组,每个元素是 劈开的子字符串段落劈开以后,匹配的位置就没有了
String[] arr = str.split(正则);

案例:

String str = "1, Tom, 110, tom@tedu.cn";
//             ,    ,    ,
//  arr=     "1" " Tom" " 110" " tom@tedu.cn"
String[] arr = str.split(",");
for(int i=0; i<arr.length; i++){System.out.println(arr[i]);
}
/*劈开ID地址*/String ip="192.168.5.25";String[]arr=ip.split("\\.");//一定要注意和"."的区别for (int i=0;i<arr.length;i++){System.out.println(arr[i]);}

replaceAll

replace: 替换

all:全部

将正则表达式匹配到的字符,都替换为新字符串

例子:

我草疫情又严重了,我去,又要做核算了。

需要替换为 ***疫情又严重了,***,又要做核算了。

代码:

Scanner scanner = new Scanner(System.in);
System.out.print("请输入:");
String str = scanner.nextLine();
//String str = "我草疫情又严重了,我去,又要做核算了。";
// str.replaceAll("正则", 替换字符串);
String s = str.replaceAll("我[去草靠艹]", "***");
System.out.println(s);

Object

包装类

二进制

什么是2进制

逢2进1的计数规则(重要)

2进制
规则:逢2进1
数字:0 1
权:128 64 32 16 8 4 2 1
基数:2

10进制计数规则

10进制
规则:逢10进1
数字:0 1 2 3 4 5 6 7 8 9
权:万 千 百 十 个
基数:10

计算机为啥是2进制?便宜!!!成本优势明显!!!

如何将2进制转换为10进制:将1位置对应权值累加求和

00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000001 = 1
00000000 00000000 00000000 00000010 = 2
00000000 00000000 00000000 00000011 = 2+1 = 3
00000000 00000000 00000000 00000100 = 4
00000000 00000000 00000000 00000101 = 4+1 = 5
00000000 00000000 00000000 00000110 = 4+2 = 6
00000000 00000000 00000000 00000111 = 4+2+1=7
00000000 00000000 00000000 00001000 = 8
00000000 00000000 00000000 00001001 = 8+1=9
00000000 00000000 00000000 00001010 = 8+2=10
00000000 00000000 00000000 00001011 = 8+2+1=11
00000000 00000000 00000000 00001100
00000000 00000000 00000000 00001101
00000000 00000000 00000000 00001110
00000000 00000000 00000000 00001111
00000000 00000000 00000000 00010000
...
00000000 00000000 00000000 00011001 = 16+8+1=25
...
00000000 00000000 00000000 01101000 = 64+32+8=104
...
package binary;public class Demo01 {public static void main(String[] args) {/** 如何查看整数的2进制存储情况* - java 编译时候,将数字编译为2进制数字* - 运行期间变量中存储的是2进制数* - 输出变量时候,Java利用API方法,将2进制转换为10进制字符串*   利用valueOf方法转换!* - Integer.toBinaryString(n) 将整数n在内存中2进制情况显示出来*/int n = 50; //n=110010System.out.println(n); //利用valueOf转换2进制为10进制字符串输出System.out.println(Integer.toBinaryString(n));System.out.println(Integer.toBinaryString(104));/** 输出0~200的2进制, 手工计算20个数的10进制值,编程验证*/for(int i=0; i<200; i++){System.out.println(Integer.toBinaryString(i));}}
}

什么是16进制

逢16进1的计数规则。

16进制用途:16进制用于缩写2进制。

  • 2进制书写非常繁琐
  • 16进制的基数是2进制的基数4次方, 2进制每4位数可以缩写为1个16进制数。
package binary;public class Demo02 {public static void main(String[] args) {/** 2进制与16进制* - Java7 提供了2进制字面量前缀 0b*   可以在数字中添加下划线,不影响数值* - 2进制直接书写非常繁琐* - 16进制缩写2进制就非常方便*   从2进制的最低位开始, 每4位数缩写为1位16进制数* - 0x是16进制的前缀* - 计算内部没有10进制,没有16进制,只有2进制!*/int n = 0b11_0010;//32+16+2=50System.out.println(n);n = 0b0001_1001_1111_0001_0100_0011_1111_0101;//    1    9    f    1    4    3    f    5System.out.println(Integer.toBinaryString(n));n = 0x19f143f5;System.out.println(Integer.toBinaryString(n));long l = 0b10111111111111111111111111111111111111L;}
}

补码

计算中一种表示有符号数编码,其核心思想就是将固定位数2进制分一般作为负数。

如何将固定位数2进制分一半作为负数?

  • 以4位2进制数讲解如何设计补码
  • 计算时候保持4位不变, 多余位自动溢出
  • 最高位称为符号位,0是正数,1是负数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qLaTMLQh-1650595231644)(image-20220125153208373.png)]

package binary;public class Demo03 {public static void main(String[] args) {/** 补码* max 最大* value 值* Integer 整数*/int n = -3;System.out.println(Integer.toBinaryString(n));int max = Integer.MAX_VALUE;int min = Integer.MIN_VALUE;System.out.println(max);System.out.println(min);System.out.println(Integer.toBinaryString(max));System.out.println(Integer.toBinaryString(min));System.out.println(Integer.toBinaryString(-1));System.out.println(Long.toBinaryString(-1L));}
}

手工计算负数的值: 计算这个数比-1少0位置的权。

11111111111111111111111111111111 = -1
11111111111111111111111111111101 = -1-2=-3
11111111111111111111111111111001 = -1-2-4=-7
11111111111111111111111111111000 = -1-1-2-4=-8
11111111111111111111111101101011 = -1-4-16-128=-149
11111111111111111111111101101100 = -1-1-2-16-128=-148
package binary;public class Demo04 {public static void main(String[] args) {/** 负数的编码* 实验: 输出-200到0的2进制编码(补码)*    随机选取20个数,手动计算10进制值*    利用Java程序验算计算结果。*/for(int i=-200; i<0; i++){System.out.println(Integer.toBinaryString(i));}}
}

long类型负数补码

1111111111111111111111111111111111111111111111111111111111111111 = -1
1111111111111111111111111111111111111111111111111111111111100110 =-26

互补对称现象:-n = ~n+1

-7    = 11111111 11111111 11111111 11111001 = -1-2-4=-7
~-7   = 00000000 00000000 00000000 00000110 = 2+4 = 6
~-7+1 = 00000000 00000000 00000000 00000111 = 1+2+4=754    =00000000 00000000 00000000 00110110= 2+4+16+32=54
~54   =11111111 11111111 11111111 11001001=-1-2-4-16-32=-55
~54+1 =11111111 11111111 11111111 11001010=-1-1-4-16-32=-54

代码:

public class Demo05 {public static void main(String[] args) {/** 验证补码的互补对称现象 -n=~n+1*/System.out.println(54);System.out.println(Integer.toBinaryString(54));System.out.println(~54);System.out.println(Integer.toBinaryString(~54));System.out.println(~54+1);System.out.println(Integer.toBinaryString(~54+1));}
}

2进制运算

运算符号:

~ 取反
& 与
| 或运算
>>> 右移位运算
>> 数学右移位运算
<< 左移位运算

& 与运算

运算规则:逻辑乘法 有0则0

0 & 0 -> 0
0 & 1 -> 0
1 & 0 -> 0
1 & 1 -> 1

运算时候将两个2进制数对其位,对应位置进行与运算

栗子:

        1   7    9   d    5   d    9   e
n   =   00010111 10011101 01011101 10011110
m   =   00000000 00000000 00000000 11111111  8位掩码
k=n&m   00000000 00000000 00000000 10011110

如上运算的意义:k中存储的是n的最后8位数,如上运算叫掩码(mask)运算。m称为mask(掩码),一般从低位开始1的个数称为掩码的位数。

代码:

int n = 0x179d5d9e;
int m = 0xff;
int k = n & m;
package binary;import java.util.ArrayList;public class Demo06 {public static void main(String[] args) {/** 掩码运算*/int n = 0x179d5d9e;//4位掩码:0xf 0b1111 15//6位掩码:0x3f 0b111111 63//8位掩码:0xff 0b11111111 255int m = 0xff; //4位 6位 8位 16位int k = n & m;System.out.println(Integer.toBinaryString(n));System.out.println(Integer.toBinaryString(m));System.out.println(Integer.toBinaryString(k));}
}

>>> 右移位运算

运算规则, 将2进制数整体向右移动,低位自动溢出舍弃,高位补0

n    =   01100111 11010111 10001111 01101101
m=n>>>1  001100111 11010111 10001111 0110110
k=n>>>2  0001100111 11010111 10001111 011011
g=n>>>8  00000000 01100111 11010111 10001111
b3 = (n>>>8) & 0xff;

代码:

int n = 0x67d78f6d;
int m = n>>>1;
int k = n>>>2;
int g = n>>>8;
int b3 = (n>>>8) & 0xff;
//按照2进制输出 n m k g b3

| 或运算

基本运算规则:逻辑加法, 有1则1

0 | 0 -> 0
0 | 1 -> 1
1 | 0 -> 1
1 | 1 -> 1

运算时候两个2进制数对齐位,对应位进行或运算

栗子:

n  =     00000000 00000000 00000000 11011101
m  =     00000000 00000000 10011101 00000000
k=n|m    00000000 00000000 10011101 11011101

上述计算的意义: 两数错位合并

代码:

int n = 0xdd;
int m = 0x9d00;
int k = n | m;
//检查 n m k 的2进制

<< 左移位运算

2进制数字整体向左移动,高位自动溢出,低位补0

栗子:

n  =    00100000 11101111 00110101 10010000
m=n<<1  0100000 11101111 00110101 100100000
k=n<<2  100000 11101111 00110101 1001000000
g=n<<8  11101111 00110101 10010000 00000000

代码:

int n = 0x20ef3590;
int m = n<<1;
int k = n<<2;
int g = n<<8;
//按照2进制输出 n m k g

移位运算的数学意义

栗子:

      16 8 4 2 11 0 1 = 51 0 1   = 10    向左移动1位  *21 0 1     = 20    向左移动2位  *2*2

代码:

int n = 5;
System.out.println(n<<1); //10
System.out.println(n<<2); //20
System.out.println(n<<3); //40
//...

>>>>> 的区别

  • >>> 逻辑右移位:数字向右移动,低位自动溢出,高位补0, 结果没有数学意义。如果仅仅将数位向右移动,不考虑数学意义,则使用>>>
  • >> 数学右移位:数学向右移动,低位自动溢出,正数高位补0,负数高位补1, 移动一次数学除以2,小方向取整数。如果是替代数学 /2, 使用数学右移位。

栗子, 使用负数比较运算结果:

n  =   11111111 11111111 11111111 11001100=-1-1-2-16-32=-52
m=n>>1 111111111 11111111 11111111 1100110=-1-1-8-16=-26
k=n>>2 1111111111 11111111 11111111 110011=-1-4-8=-13
g=n>>3 11111111111 11111111 11111111 11001=-1-2-4=-7
n>>>1  011111111 11111111 11111111 1100110=max-25没有数学意义

程序:

int n = -52; //0xffffffcc;
int m = n>>1;
int k = n>>2;
int g = n>>3;
int x = n>>>1;
//输出n m k g x

将一个整数拆分为4个字节

栗子

            b1       b2       b3       b4
n   =    00010111 10011101 01011101 10011110
b1  =    00000000 00000000 00000000 00010111
b2  =    00000000 00000000 00000000 10011101
b3  =    00000000 00000000 00000000 01011101
b4  =    00000000 00000000 00000000 10011110

代码:n =-1 n=-3 n=max n=min

int n = 0x179d5d9e;
int b1 = (n >>> 24) & 0xff;
int b2 = (n >>> 16) & 0xff;
int b3 = (n >>> 8) & 0xff;
int b4 = n & 0xff;
//验证:按照二进制输出 n b1 b2 b3 b4
//n=-1 时候按照10进制输出是啥结果?

将4个字节合并为一个整数

b1  =    00000000 00000000 00000000 00010111
b2  =    00000000 00000000 00000000 10011101
b3  =    00000000 00000000 00000000 01011101
b4  =    00000000 00000000 00000000 10011110b1<<24   00010111 00000000 00000000 00000000
b2<<16   00000000 10011101 00000000 00000000
b3<<8    00000000 00000000 01011101 00000000
b4       00000000 00000000 00000000 10011110n = (b1<<24) | (b2<<16) | (b3<<8) | b4;

代码:

int b1 = 0x17;
int b2 = 0x9d;
int b3 = 0x5d;
int b4 = 0x9e;
int n = (b1<<24) | (b2<<16) | (b3<<8) | b4;
//按照2进制输出 b1 b2 b3 b3 n

Object类

Object是所有类的顶级超类,其中有两个经常被子类重写的方法:

toString()与equals().

import java.util.Objects;/*** 使用当前类学习常备子类重写的Object中的相关方法*/
public class Point {private int x;private int y;public Point(int x, int y) {this.x = x;this.y = y;}public int getX() {return x;}public void setX(int x) {this.x = x;}public int getY() {return y;}public void setY(int y) {this.y = y;}@Overridepublic String toString() {return "Point{" +"x=" + x +", y=" + y +'}';}@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;Point point = (Point) o;return x == point.x && y == point.y;}@Overridepublic int hashCode() {return Objects.hash(x, y);}
}
package object;/*** Object是所有类的顶级超类.里面有几个经常被子类重写的方法,其中包括toString和equals*/
public class Demo {public static void main(String[] args) {Point p = new Point(1,2);/*Object已经实现了toString方法.直接继承下来时返回的字符串内容为当前对象的地址信息.格式为:类名@地址.toString方法实际开发中很少直接写代码去调用它,都是在不经意间被自动执行的.例如在使用System.out.println输出时.与字符串连接操作时.*///System.out.println(Object obj)输出给定对象toString返回的字符串到控制台System.out.println(p);//字符串连接操作时,会将非String类型的对象调用toString转换为字符串后拼接.String line = "这是一个Point:" + p;System.out.println(line);Point p2 = new Point(1,2);System.out.println("p2:"+p2);/*对于引用类型而言,变量保存的值是对象的地址.==比较是比较两个变量的值是否相等,因此对于引用类型而言就是比较地址是否相等,那么意思就是比较是否为同一个对象.equals是Object定义的另一个方法,意图是比较两个对象的内容是否相同.但是如果子类不重写这个方法,则没有实际意义,因为Object实现时内部仍然是用==比较的!*/System.out.println(p == p2);//falseSystem.out.println(p.equals(p2));//true}
}

包装类

java定义了8个包装类,目的是为了解决基本类型不能直接参与面向对象开发的问题,使得基本类型可以通过包装类的实例以对象的形式存在.

  • 其中数字类型的包装类都继承自java.lang.Number,而char和boolean的包装类直接继承自Object
  • Number是一个抽象类,定义了一些方法,目的是让包装类可以将其表示的基本类型转换为其他数字类型.
package integer;public class IntegerDemo1 {public static void main(String[] args) {//基本类型转换为包装类int i = 123;//java推荐我们使用包装类的静态方法valueOf将基本类型转换为包装类,而不是直接newInteger i1 = Integer.valueOf(i);//Integer会重用-128-127之间的整数对象Integer i2 = Integer.valueOf(i);System.out.println(i1==i2);//trueSystem.out.println(i1.equals(i2));//truedouble dou = 123.123;Double dou1 = Double.valueOf(dou);//Double则是直接newDouble dou2 = Double.valueOf(dou);System.out.println(dou1==dou2);//falseSystem.out.println(dou1.equals(dou2));//true//包装类转换为基本类型int in = i1.intValue();//获取包装类对象中表示的基本类型值double doub = i1.doubleValue();System.out.println(in);//123System.out.println(doub);//123.0in = dou1.intValue();//大类型转小类型可能存在丢精度!doub = dou1.doubleValue();System.out.println(in);//123System.out.println(doub);//123.123}
}
包装类常用功能
package integer;public class IntegerDemo2 {public static void main(String[] args) {//1可以通过包装类获取其表示的基本类型的取值范围//获取int的最大值和最小值?int imax = Integer.MAX_VALUE;System.out.println(imax);int imin = Integer.MIN_VALUE;System.out.println(imin);long lmax = Long.MAX_VALUE;System.out.println(lmax);long lmin = Long.MIN_VALUE;System.out.println(lmin);/*2字符串转换为基本类型的前提是该字符串正确描述了基本类型可以保存的值,否则会抛出异常:NumberFormatException*/String str = "123";
//        String str = "123.123";//这个字符串不能解析为int值!int d = Integer.parseInt(str);System.out.println(d);//123double dou = Double.parseDouble(str);System.out.println(dou);//123.123}
}
自动拆装箱特性

JDK5之后推出了一个新的特性:自动拆装箱

该特性是编译器认可的.当编译器编译源代码时发现有基本类型和引用类型相互赋值使用时会自动补充代码来完成他们的转换工作,这个过程称为自动拆装箱.

package integer;public class IntegerDemo3 {public static void main(String[] args) {/*触发自动拆箱特性,编译器会补充代码将包装类转换为基本类型,下面的代码会变为:int i = new Integer(123).intValue();*/int i = new Integer(123);/*触发编译器自动装箱特性,代码会被编译器改为:Integer in = Integer.valueOf(123);*/Integer in = 123;}
}

File类

File类的每一个实例可以表示硬盘(文件系统)中的一个文件或目录(实际上表示的是一个抽象路径)

使用File可以做到:

  • 1:访问其表示的文件或目录的属性信息,例如:名字,大小,修改时间等等
  • 2:创建和删除文件或目录
  • 3:访问一个目录中的子项

但是File不能访问文件数据.

public class FileDemo {public static void main(String[] args) {//使用File访问当前项目目录下的demo.txt文件/*创建File时要指定路径,而路径通常使用相对路径。相对路径的好处在于有良好的跨平台性。"./"是相对路径中使用最多的,表示"当前目录",而当前目录是哪里取决于程序运行环境而定,在idea中运行java程序时,这里指定的当前目录就是当前程序所在的项目目录。*/
//        File file = new File("c:/xxx/xxx/xx/xxx.txt");File file = new File("./demo.txt");//获取名字String name = file.getName();System.out.println(name);//获取文件大小(单位是字节)long len = file.length();System.out.println(len+"字节");//是否可读可写boolean cr = file.canRead();boolean cw = file.canWrite();System.out.println("是否可读:"+cr);System.out.println("是否可写:"+cw);//是否隐藏boolean ih = file.isHidden();System.out.println("是否隐藏:"+ih);}}
创建一个新文件

createNewFile()方法,可以创建一个新文件

package file;import java.io.File;
import java.io.IOException;/*** 使用File创建一个新文件*/
public class CreateNewFileDemo {public static void main(String[] args) throws IOException {//在当前目录下新建一个文件:test.txtFile file = new File("./test.txt");//boolean exists()判断当前File表示的位置是否已经实际存在该文件或目录if(file.exists()){System.out.println("该文件已存在!");}else{file.createNewFile();//将File表示的文件创建出来System.out.println("文件已创建!");}}
}
删除一个文件

delete()方法可以将File表示的文件删除

package file;import java.io.File;/*** 使用File删除一个文件*/
public class DeleteFileDemo {public static void main(String[] args) {//将当前目录下的test.txt文件删除/*相对路径中"./"可以忽略不写,默认就是从当前目录开始的。*/File file = new File("test.txt");if(file.exists()){file.delete();System.out.println("文件已删除!");}else{System.out.println("文件不存在!");}}
}
创建目录

mkDir():创建当前File表示的目录

mkDirs():创建当前File表示的目录,同时将所有不存在的父目录一同创建

package file;import java.io.File;/*** 使用File创建目录*/
public class MkDirDemo {public static void main(String[] args) {//在当前目录下新建一个目录:demo
//        File dir = new File("demo");File dir = new File("./a/b/c/d/e/f");if(dir.exists()){System.out.println("该目录已存在!");}else{//            dir.mkdir();//创建目录时要求所在的目录必须存在dir.mkdirs();//创建目录时会将路径上所有不存在的目录一同创建System.out.println("目录已创建!");}}
}
删除目录

delete()方法可以删除一个目录,但是只能删除空目录。

package file;import java.io.File;/*** 删除一个目录*/
public class DeleteDirDemo {public static void main(String[] args) {//将当前目录下的demo目录删除File dir = new File("demo");
//        File dir = new File("a");if(dir.exists()){dir.delete();//delete方法删除目录时只能删除空目录System.out.println("目录已删除!");}else{System.out.println("目录不存在!");}}
}
访问一个目录中的所有子项

listFiles方法可以访问一个目录中的所有子项

package file;import java.io.File;/*** 访问一个目录中的所有子项*/
public class ListFilesDemo1 {public static void main(String[] args) {//获取当前目录中的所有子项File dir = new File(".");/*boolean isFile()判断当前File表示的是否为一个文件boolean isDirectory()判断当前File表示的是否为一个目录*/if(dir.isDirectory()){/*File[] listFiles()将当前目录中的所有子项返回。返回的数组中每个File实例表示其中的一个子项*/File[] subs = dir.listFiles();System.out.println("当前目录包含"+subs.length+"个子项");for(int i=0;i<subs.length;i++){File sub = subs[i];System.out.println(sub.getName());}}}
}
获取目录中符合特定条件的子项

重载的listFiles方法:File[] listFiles(FileFilter)

该方法要求传入一个文件过滤器,并仅将满足该过滤器要求的子项返回。

package file;import java.io.File;
import java.io.FileFilter;/*** 重载的listFiles方法,允许我们传入一个文件过滤器从而可以有条件的获取一个目录* 中的子项。*/
public class ListFilesDemo2 {public static void main(String[] args) {/*需求:获取当前目录中所有名字以"."开始的子项*/File dir = new File(".");if(dir.isDirectory()){//            FileFilter filter = new FileFilter(){//匿名内部类创建过滤器
//                public boolean accept(File file) {//                    String name = file.getName();
//                    boolean starts = name.startsWith(".");//名字是否以"."开始
//                    System.out.println("过滤器过滤:"+name+",是否符合要求:"+starts);
//                    return starts;
//                }
//            };
//            File[] subs = dir.listFiles(filter);//方法内部会调用accept方法File[] subs = dir.listFiles(new FileFilter(){public boolean accept(File file) {return file.getName().startsWith(".");}});System.out.println(subs.length);}}
}

使用递归操作删除一个目录

循环是重复执行某个步骤,而递归是重复整个过程。

package file;import java.io.File;/*** 编写一个程序,要求实现1+2+3+4+....100并输出结果。* 代码中不能出现for,while关键字** 编写程序计算:* 一个人买汽水,1块钱1瓶汽水。3个瓶盖可以换一瓶汽水,2个空瓶可以换一瓶汽水。不考虑赊账问题* 问20块钱可以最终得到多少瓶汽水。** 删除一个目录*/
public class Test {public static void main(String[] args) {File dir = new File("./a");delete(dir);}/*** 将给定的File对象表示的文件或目录删除* @param file*/public static void delete(File file){if(file.isDirectory()) {//清空目录File[] subs = file.listFiles();for (int i = 0; i < subs.length; i++) {File sub = subs[i];//从目录中获取一个子项//将该子项删除delete(sub);//递归调用}}file.delete();}
}

Lambda表达式

JDK8之后,java支持了lambda表达式这个特性.

  • lambda可以用更精简的代码创建匿名内部类.但是该匿名内部类实现的接口只能有一个抽象方法,否则无法使用!
  • lambda表达式是编译器认可的,最终会将其改为内部类编译到class文件中
package lambda;import java.io.File;
import java.io.FileFilter;/*** JDK8之后推出了一个新的特性:lambda表达式* lambda表达式可以用更精简的方式创建匿名内部类。* 语法:* (参数列表)->{*     方法体* }* 不是所有匿名内部类创建的情景都可以替换为lambda.前提要求是实现的接口只能有一个抽象方法时才可以* 使用lambda表达式。*/
public class LambdaDemo {public static void main(String[] args) {FileFilter filter = new FileFilter() {public boolean accept(File file) {return file.getName().contains("o");}};//忽略了接口名和方法名FileFilter filter1 = (File file)->{return file.getName().contains("o");};//参数类型可以忽略。如果方法只有一个参数时,那么"()"也可以忽略。FileFilter filter2 = file->{return file.getName().contains("o");};//如果方法体只有一句代码,那么可以将方法体"{}"忽略,如果含有return则return要一同忽略FileFilter filter3 = file->file.getName().contains("o");File dir = new File(".");
//        File[] subs = dir.listFiles(new FileFilter() {//            public boolean accept(File file) {//                return file.getName().contains("o");
//            }
//        });/*lambda表达式也是编译器认可的,最终会被编译器改回成内部类创建的形式*/File[] subs = dir.listFiles(f->f.getName().contains("o"));}
}

JAVA IO

  • java io可以让我们用标准的读写操作来完成对不同设备的读写数据工作.

  • java将IO按照方向划分为输入与输出,参照点是我们写的程序.

  • 输入:用来读取数据的,是从外界到程序的方向,用于获取数据.

  • 输出:用来写出数据的,是从程序到外界的方向,用于发送数据.

    java将IO比喻为"流",即:stream. 就像生活中的"电流",“水流"一样,它是以同一个方向顺序移动的过程.只不过这里流动的是字节(2进制数据).所以在IO中有输入流和输出流之分,我们理解他们是连接程序与另一端的"管道”,用于获取或发送数据到另一端.

Java定义了两个超类(抽象类):
  • java.io.InputStream:所有字节输入流的超类,其中定义了读取数据的方法.因此将来不管读取的是什么设备(连接该设备的流)都有这些读取的方法,因此我们可以用相同的方法读取不同设备中的数据
  • java.io.OutputStream:所有字节输出流的超类,其中定义了写出数据的方法.
java将流分为两类:节点流与处理流:
  • 节点流:也称为低级流.节点流的另一端是明确的,是实际读写数据的流,读写一定是建立在节点流基础上进行的.
  • 处理流:也称为高级流.处理流不能独立存在,必须连接在其他流上,目的是当数据流经当前流时对数据进行加工处理来简化我们对数据的该操作.
实际应用中,我们可以通过串联一组高级流到某个低级流上以流水线式的加工处理对某设备的数据进行读写,这个过程也成为流的连接,这也是IO的精髓所在.

文件流

文件流是一对低级流,用于读写文件数据的流.用于连接程序与文件(硬盘)的"管道".负责读写文件数据.

文件输出流:java.io.FileOutputStream
package io;import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;/*** JAVA IO  Input&Output  输入和输出* java程序与外界交换数据是基于IO完成的,这里输入与输出一个负责读一个负责写* 输入:是从外界到我们写的程序的方向,是用来从外界获取信息的。因此是"读"操作* 输出:是从我们写的程序到外界的方向,是用来向外界发送信息的。因此是"写"操作** java将IO比喻为"流",可以理解为是程序与外界连接的"管道",内部流动的是字节,并且* 字节是顺着同一侧方向顺序移动的。** java.io.InputStream 输入流,这个类是所有字节输入流的超类,规定了所有字节输入* 流读取数据的相关方法。* java.io.OutputStream 输出流,这个类是所有字节输出流的超类,规定了所有字节输出* 流写出数据的相关方法。** 实际应用中,我们连接不同的设备,java都专门提供了用于连接它的输入流与输出流,而* 这些负责实际连接设备的流称为节点流,也叫低级流。是真实负责读写数据的流。* 与之对应的还有高级流,高级流可以连接其他的流,目的是当数据流经它们时,对数据做某* 种加工处理,用来简化我们的操作。*** 文件流* java.io.FileInputStream和FileOutputStream* 这是一对低级流,继承自InputStream和OutputStream。用于读写硬盘上文件的流**/
public class FOSDemo {public static void main(String[] args) throws IOException {//向当前目录下的demo.dat文件中写入数据/*FileOutputStream提供的常用构造器FileOutputStream(String path)FileOutputStream(File file)*///文件流创建时,如果该文件不存在会自动将其创建(前提是该文件所在目录必须存在!)FileOutputStream fos = new FileOutputStream("./demo.dat");/*void write(int d)向文件中写入1个字节,写入的内容是给定的int值对应的2进制的"低八位"int值 1:                         vvvvvvvv二进制:00000000 00000000 00000000 00000001demo.dat文件内容:00000000*/fos.write(1);/*vvvvvvvv00000000 00000000 00000000 00000010demo.dat文件内容00000001 00000010*/fos.write(2);fos.close();System.out.println("执行完了!");}
}
文件输入流
package io;import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;/*** end 结尾* read 读*** 文件字节输入流,用于从文件中读取字节*/
public class FISDemo {public static void main(String[] args) throws IOException {/*fos.dat文件内容00000001 00000011*/FileInputStream fis = new FileInputStream("fos.dat");/*int read()读取一个字节,并一int型返回。返回的整数中读取的字节部分在该整数2进制的最后8位上如果返回值为整数-1,则表示流读取到了末尾。对于读取文件而言就是EOF(end of file文件末尾)第一次调用read():int d = fis.read();fos.dat文件内容00000001 00000011^^^^^^^^读取该字节返回int值时,2进制样子:00000000 00000000 00000000 00000001^^^^^^^^|-----补充24个0(3字节)-----| 读取的字节返回的int值d就是上述内容*/int d = fis.read();System.out.println(d);/*第二次调用read()d = fis.read();fos.dat文件内容00000001 00000011^^^^^^^^读取该字节返回int值时,2进制样子:00000000 00000000 00000000 00000011^^^^^^^^|-----补充24个0(3字节)-----| 读取的字节返回的int值d就是上述内容*/d = fis.read();System.out.println(d);/*第三次调用read()d = fis.read();fos.dat文件内容00000001 00000011^^^^^^^^文件末尾了返回int值时,2进制样子:11111111 11111111 11111111 11111111^^^^^^^^|-----补充32个1(4字节,来表示-1)-----|返回的int值d就是上述内容*/d = fis.read();System.out.println(d);fis.close();}
}

文件复制

package io;import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;/*** 文件的复制*/
public class CopyDemo {public static void main(String[] args) throws IOException {//创建文件输入流读取原文件FileInputStream fis = new FileInputStream("image.jpg");//创建文件输出流写入复制文件FileOutputStream fos = new FileOutputStream("image_cp.jpg");int d;//保存每次读取到的字节/*原文件数据:11000011 10101010 00001111 11001100 00110011 ...^^^^^^^^d = fis.read();d:00000000 00000000 00000000 10101010fos.write(d);复制文件的数据:11000011 10101010*/long start = System.currentTimeMillis();//获取当前系统时间的毫秒值(UTC时间)while((d = fis.read()) != -1) {fos.write(d);}long end = System.currentTimeMillis();//获取当前系统时间的毫秒值(UTC时间)System.out.println("复制完毕!耗时:"+(end-start)+"ms");fis.close();fos.close();}
}

块读写的文件复制操作

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

块写操作
void write(byte[] data)
一次性将给定的字节数组所有字节写入到文件中

void write(byte[] data,int offset,int len)
一次性将给定的字节数组从下标offset处开始的连续len个字节写入文件

package io;import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;/*** 通过提高每次读写的数据量,减少实际读写的次数,可以提高读写效率。* 单字节读写是一种随机读写形式。而一组一组字节的读写是块读写形式。*/
public class CopyDemo2 {public static void main(String[] args) throws IOException {//使用块读写形式完成文件复制//创建文件输入流读取原文件FileInputStream fis = new FileInputStream("wnwb.exe");//创建文件输出流写复制文件FileOutputStream fos = new FileOutputStream("wnwb_cp.exe");/*流提供了块读写的方法int read(byte[] data)一次性从文件中读取给定的字节数组总长度的字节量,并存入到该数组中。返回值为实际读取到的字节量。若返回值为-1则表示读取到了文件末尾。文件数据11001100 11110000 10101010 00001111 00110011^^^^^^^^ ^^^^^^^^ ^^^^^^^^int d;byte[] data = new byte[3];[00000000 00000000 00000000]第一次调用d = fis.read(data);[11001100 11110000 10101010]d = 3 本次读取到了3个字节文件数据11001100 11110000 10101010 00001111 00110011^^^^^^^^ ^^^^^^^^第二次调用d = fis.read(data);//仅读取了最后两个字节[00001111 00110011 10101010]//前两个字节为本次读取的内容^^^^^^^^ ^^^^^^^^d = 2 本次读取到了2个字节文件数据11001100 11110000 10101010 00001111 00110011 文件末尾!^^^^^^^^第三次调用d = fis.read(data);//一个字节都没有读取到[00001111 00110011 10101010]数组没变化d = -1 文件末尾块写操作void write(byte[] data)一次性将给定的字节数组所有字节写入到文件中void write(byte[] data,int offset,int len)一次性将给定的字节数组从下标offset处开始的连续len个字节写入文件*/int len;//记录每次实际读取的字节量/*00000000  1byte   8位2进制称为1字节1024byte  1kb1024kb    1mb1024mb    1gb*/byte[] data = new byte[1024*10];//10kblong start = System.currentTimeMillis();while((len = fis.read(data))!=-1){fos.write(data,0,len);//读取多少就写多少}long end = System.currentTimeMillis();System.out.println("复制完毕!耗时:"+(end-start)+"ms");fis.close();fos.close();}
}

写文本数据

String提供方法:
byte[] getBytes(String charsetName)
将当前字符串转换为一组字节

参数为字符集的名字,常用的是UTF-8。 其中中文字3字节表示1个,英文1字节表示1个。

package io;import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;/*** 向文件中写入文本数据*/
public class WriteStringDemo {public static void main(String[] args) throws IOException {FileOutputStream fos = new FileOutputStream("demo.txt");String str = "super idol的笑容都没你的甜,";/*支持中文的常见字符集有:GBK:国标编码。英文每个字符占1个字节,中文每个字符占2个字节UTF-8:内部是unicode编码,在这个基础上不同了少部分2进制信息作为长度描述英文每个字符占1字节中文每个字符占3字节String提供了将字符串转换为一组字节的方法byte[] getBytes(String charsetName)参数为字符集的名字,名字不缺分大小写,但是拼写错误会引发异常:UnsupportedEncodingException不支持      字符集   异常*/byte[] data = str.getBytes("UTF-8");fos.write(data);fos.write("八月正午的阳光,都没你耀眼。".getBytes("UTF-8"));System.out.println("写出完毕!");fos.close();}
}

文件输出流-追加模式

重载的构造方法可以将文件输出流创建为追加模式

  • FileOutputStream(String path,boolean append)
  • FileOutputStream(File file,boolean append)

当第二个参数传入true时,文件流为追加模式,即:指定的文件若存在,则原有数据保留,新写入的数据会被顺序的追加到文件中

package io;import java.io.FileOutputStream;
import java.io.IOException;/*** 文件流的追加写模式*/
public class FileAppendDemo {public static void main(String[] args) throws IOException {/*FileOutputStream默认创建方式为覆盖模式,即:如果连接的文件存在,则会将该文件原有数据全部删除。然后将通过当前流写出的内容保存到文件中。重载的构造方法允许我们再传入一个boolean型参数,如果这个值为true,则文件流为追加模式,即:若连接文件时该文件存在,原有数据全部保留,通过当前流写出的数据会顺序的追加到文件中。*/FileOutputStream fos = new FileOutputStream("demo.txt",true);fos.write("热爱105°的你,".getBytes("UTF-8"));fos.write("滴滴清纯的蒸馏水。".getBytes("UTF-8"));System.out.println("写出完毕!");fos.close();}
}

读取文本数据

package io;import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;/*** 从文件中读取文本数据*/
public class ReadStringDemo {public static void main(String[] args) throws IOException {FileInputStream fis = new FileInputStream("fos.txt");byte[] data = new byte[1024];int len = fis.read(data);//块读操作System.out.println("实际读取到了"+len+"个字节");/*String提供了将字节数组转换为字符串的构造方法:String(byte[]data,String charsetName)将给定的字节数组中所有字节按照指定的字符集转换为字符串String(byte[]data,int offset,int len,String charsetName)将给定的字节数组从下标offset处开始的连续len个字节按照指定的字符集转换为字符串*/String line = new String(data,0,len,"UTF-8");System.out.println(line);System.out.println(line.length());fis.close();}
}

高级流

流连接示意图

缓冲流

java.io.BufferedOutputStream和BufferedInputStream.

缓冲流是一对高级流,作用是提高读写数据的效率.

缓冲流内部有一个字节数组,默认长度是8K.缓冲流读写数据时一定是将数据的读写方式转换为块读写来保证读写效率.

使用缓冲流完成文件复制操作

package io;import java.io.*;/*** java将流分为节点流与处理流两类* 节点流:也称为低级流,是真实连接程序与另一端的"管道",负责实际读写数据的流。*       读写一定是建立在节点流的基础上进行的。*       节点流好比家里的"自来水管"。连接我们的家庭与自来水厂,负责搬运水。* 处理流:也称为高级流,不能独立存在,必须连接在其他流上,目的是当数据经过当前流时*       对其进行某种加工处理,简化我们对数据的同等操作。*       高级流好比家里常见的对水做加工的设备,比如"净水器","热水器"。*       有了它们我们就不必再自己对水进行加工了。* 实际开发中我们经常会串联一组高级流最终连接到低级流上,在读写操作时以流水线式的加工* 完成复杂IO操作。这个过程也称为"流的连接"。** 缓冲流,是一对高级流,作用是加快读写效率。* java.io.BufferedInputStream和java.io.BufferedOutputStream**/
public class CopyDemo3 {public static void main(String[] args) throws IOException {FileInputStream fis = new FileInputStream("ppt.pptx");BufferedInputStream bis = new BufferedInputStream(fis);FileOutputStream fos = new FileOutputStream("ppt_cp.pptx");BufferedOutputStream bos = new BufferedOutputStream(fos);int d;long start = System.currentTimeMillis();while((d = bis.read())!=-1){//使用缓冲流读取字节bos.write(d);//使用缓冲流写出字节}long end = System.currentTimeMillis();System.out.println("耗时:"+(end-start)+"ms");bis.close();//关闭流时只需要关闭高级流即可,它会自动关闭它连接的流bos.close();}
}
缓冲输出流写出数据时的缓冲区问题

通过缓冲流写出的数据会被临时存入缓冲流内部的字节数组,直到数组存满数据才会真实写出一次

package io;import java.io.BufferedOutputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;/*** 缓冲输出流写出数据的时效性问题(缓冲区问题)*/
public class BOSDemo {public static void main(String[] args) throws IOException {FileOutputStream fos = new FileOutputStream("bos.txt");BufferedOutputStream bos = new BufferedOutputStream(fos);String line = "画画的baby,画画的baby,奔驰的小野马和带刺的玫瑰~";byte[] data = line.getBytes("UTF-8");bos.write(data);/*void flush()将缓冲流的缓冲区中已缓存的数据一次性写出。注:频繁调用该方法会提高写出的次数降低写出效率。但是可以换来写出数据的即时性。*/bos.flush();System.out.println("写出完毕!");/*缓冲流的close操纵中会自动调用一次flush,确保所有缓存的数据会被写出*/bos.close();}
}

对象流

java.io.ObjectOutputStream和ObjectInputSteam

对象流是一对高级流,在流连接中的作用是进行对象的序列化与反序列化。

对象序列化:将一个java对象按照其结构转换为一组字节的过程

对象反序列化:将一组字节还原为java对象(前提是这组字节是一个对象序列化得到的字节)

对象序列化的流连接操作原理图:

package io;import java.io.Serializable;
import java.util.Arrays;/*** 使用当前类测试对象流的序列化与反序列化操作* 对象序列化:将一个java对象按照其结构转换为一组字节的过程**/
public class Person implements Serializable {/*当一个类实现了可序列化接口时,最好显示的定义下面的属性:serialVersionUID即:序列化版本号当对象输出流在进行对象序列化时,会查看是否有显示的定义版本号,如果没有则会根据当前类的结构计算版本号(当前类(这里是Person)的结构只要没有发生过变化,那么无论何时序列化版本号始终不会变化,只要改变过,那么序列化版本号一定会改变)并将该版本号保存在序列化后的字节中。重点:当使用对象输入流反序列化时,对象输入流会检查要反序列化的对象与其对应的类(比如我们反序列化一个之前的Person对象)的版本号是否一致,若不一致,则会抛出异常:java.io.InvalidClassException比如OOSDemo序列化一个Person对象并写入文件person.obj后。我们在Person上添加一个新的属性salary,此时Person类发生了变化,那么再使用OISDemo反序列化person.obj文件中之前序列化的对象时就会发生异常InvalidClassException(因为Person类改变了结构,版本号不一致了)。如果后期修改了类结构,又希望原来的对象还可以进行反序列化,则需要显示的定义出来序列化版本号,这样一来,当一个对象序列化后,当前类结构改变了,只要版本号不变,那么之前的对象仍然可以进行反序列化。此时对象输入流会采取兼容方式,即:能还原的属性都会进行还原,没有的属性则采用默认值。*/static final long serialVersionUID = 1L;private String name;//姓名private int age;//年龄private String gender;//性别/*当一个属性被关键字transient修饰后,那么当进行对象序列化时,该属性的值会被忽略。忽略不必要的属性可以达到对象"瘦身"的目的,提高程序响应速度,减少资源开销。当然,反序列化时,该属性只会采用默认值。transient:短暂的,转瞬即逝*/private transient String[] otherInfo;//其他信息//    private int salary;//工资//alt+insert 自动生成代码的快捷键public Person(String name, int age, String gender, String[] otherInfo) {this.name = name;this.age = age;this.gender = gender;this.otherInfo = otherInfo;}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 String getGender() {return gender;}public void setGender(String gender) {this.gender = gender;}public String[] getOtherInfo() {return otherInfo;}public void setOtherInfo(String[] otherInfo) {this.otherInfo = otherInfo;}@Overridepublic String toString() {return "Person{" +"name='" + name + '\'' +", age=" + age +", gender='" + gender + '\'' +", otherInfo=" + Arrays.toString(otherInfo) +'}';}
}
package io;import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;/*** 对象流* java.io.ObjectOutputStream和ObjectInputSteam* 对象流是一对高级流,在流连接中的作用是进行对象的序列化与反序列化** 对象序列化:将一个java对象按照其结构转换为一组字节的过程* 对象反序列化:将一组字节还原为java对象(前提是这组字节是一个对象序列化得到的字节)*/
public class OOSDemo {public static void main(String[] args) throws IOException {//将一个Person对象写入文件person.objString name = "苍#null";int age = 18;String gender = "女";String[] otherInfo = {"是一名台词不多的演员","来自岛国","爱好写大字","广大男性同胞的启蒙老师"};Person p = new Person(name,age,gender,otherInfo);System.out.println(p);FileOutputStream fos = new FileOutputStream("person.obj");ObjectOutputStream oos = new ObjectOutputStream(fos);oos.writeObject(p);System.out.println("写出完毕!");oos.close();}
}
对象反序列化
package io;import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectInputStream;/*** 使用对象输入流完成对象的反序列化*/
public class OISDemo {public static void main(String[] args) throws IOException, ClassNotFoundException {//从person.obj文件中将对象反序列化回来FileInputStream fis = new FileInputStream("person.obj");ObjectInputStream ois = new ObjectInputStream(fis);/*Object readObject()该方法会进行对象的反序列化,如果对象流通过其连接的流读取的字节分析并非是一个java对象时,会抛出异常:ClassNotFoundException*/Person p = (Person)ois.readObject();System.out.println(p);}
}
package io;import java.io.Serializable;
import java.util.Arrays;/*** 使用当前类实例测试对象流的读写操作*/
public class Person implements Serializable {private String name;//姓名private int age;//年龄private String gender;//性别private String[] otherInfo;//其他信息public Person(String name, int age, String gender, String[] otherInfo) {this.name = name;this.age = age;this.gender = gender;this.otherInfo = otherInfo;}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 String getGender() {return gender;}public void setGender(String gender) {this.gender = gender;}public String[] getOtherInfo() {return otherInfo;}public void setOtherInfo(String[] otherInfo) {this.otherInfo = otherInfo;}@Overridepublic String toString() {return "Person{" +"name='" + name + '\'' +", age=" + age +", gender='" + gender + '\'' +", otherInfo=" + Arrays.toString(otherInfo) +'}';}
}

字符流

  • java将流按照读写单位划分为字节流与字符流.
  • java.io.InputStream和OutputStream是所有字节流的超类
  • 而java.io.Reader和Writer则是所有字符流的超类,它们和字节流的超类是平级关系.
  • Reader和Writer是两个抽象类,里面规定了所有字符流都必须具备的读写字符的相关方法.
  • 字符流最小读写单位为字符(char),但是底层实际还是读写字节,只是字符与字节的转换工作由字符流完成.

转换流

java.io.InputStreamReader和OutputStreamWriter

它们是字符流非常常用的一对实现类同时也是一对高级流,实际开发中我们不直接操作它们,但是它们在流连接中是非常重要的一环.

使用转换输出流向文件中写入文本数据

package io;import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;/*** 字符流* java将流按照读写单位划分为字节流与字符流.* java.io.InputStream和OutputStream是所有字节流的超类* 而java.io.Reader和Writer则是所有字符流的超类,它们是平级关系.** Reader和Writer是两个抽象类,里面规定了所有字符流都必须具备的读写字符的相关方法.* 字符流最小读写单位为字符(char),但是底层实际还是读写字节,只是字符与字节的转换工作由* 字符流完成.** 转换流* java.io.InputStreamReader和OutputStreamWriter* 它们是字符流非常常用的一对实现类同时也是一对高级流,实际开发中我们不直接操作它们,但是* 它们在流连接中是非常重要的一环.*/
public class OSWDemo {public static void main(String[] args) throws IOException {//向文件osw.txt中写入文字FileOutputStream fos = new FileOutputStream("osw.txt");OutputStreamWriter osw = new OutputStreamWriter(fos,"UTF-8");osw.write("我可以接受你的所有,所有小脾气.");osw.write("我可以带你去吃很多,很多好东西.");System.out.println("写出完毕!");osw.close();}
}
使用转换输入流读取文本文件
package io;import java.io.*;/*** 转换字符输入流* 可以将读取的字节按照指定的字符集转换为字符*/
public class ISRDemo {public static void main(String[] args) throws IOException {//将osw.txt文件中的所有文字读取回来.FileInputStream fis = new FileInputStream("osw.txt");InputStreamReader isr = new InputStreamReader(fis,"UTF-8");/*字符流读一个字符的read方法定义:int read()读取一个字符,返回的int值实际上表示的是一个char(低16位有效).如果返回的int值表示的是-1则说明EOF*///测试读取文件中第一个字
//        int d = isr.read();
//        char c = (char)d;
//        System.out.println(c);//循环将文件所有字符读取回来int d;while((d = isr.read()) != -1){System.out.print((char)d);}isr.close();}
}
转换流的意义:

实际开发中我们还有功能更好用的字符高级流.但是其他的字符高级流都有一个共通点:不能直接连接在字节流上.而实际操作设备的流都是低级流同时也都是字节流.因此不能直接在流连接中串联起来.转换流是一对可以连接在字节流上的字符流,其他的高级字符流可以连接在转换流上.在流连接中起到"转换器"的作用(负责字符与字节的实际转换)

缓冲字符流

缓冲字符输出流:java.io.PrintWriter

java.io.BufferedWriter和BufferedReader

缓冲字符流内部也有一个缓冲区,读写文本数据以块读写形式加快效率.并且缓冲流有一个特别的功能:可以按行读写文本数据.

java.io.PrintWriter具有自动行刷新的缓冲字符输出流,实际开发中更常用.它内部总是会自动连接BufferedWriter作为块写加速使用.

package io;import java.io.FileNotFoundException;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;/*** 缓冲字符流* 缓冲字符流是一对高级流,在流连接中的作用是提高读写文本数据的效率,并且* 可以安行读写字符串.* java.io.BufferedReader和BufferedWriter** 实际开发中缓冲字符输出流我们更常用的是PrintWriter,具有自动行刷新功能* 的缓冲字符输出流,其内部总是连接BufferedWriter作为缓冲加速使用.*/
public class PWDemo1 {public static void main(String[] args) throws FileNotFoundException, UnsupportedEncodingException {/*PrintWriter提供了对文件操作的构造方法:PrintWriter(String path)PrintWriter(File file)*///向文件中写入字符串PrintWriter pw = new PrintWriter("pw.txt","UTF-8");pw.println("我看过沙漠下暴雨");pw.println("看过大海亲吻鲨鱼");pw.println("看过黄昏追逐黎明");pw.println("没看过你");System.out.println("写出完毕!");pw.close();}
}
在流链接中使用PW
package io;import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;/*** 在流连接中使用PW*/
public class PWDemo2 {public static void main(String[] args) throws FileNotFoundException {//文件字节输出流(是一个低级流),向文件中写入字节数据FileOutputStream fos = new FileOutputStream("pw2.txt",true);//转换输出流(是一个高级流,且是一个字符流)。1:衔接字符与字节流 2:将写出的字符转换为字节OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8);//缓冲输出流(是一个高级流,且是一个字符流)。块写文本数据加速BufferedWriter bw = new BufferedWriter(osw);//具有自动行刷新的缓冲字符输出流PrintWriter pw = new PrintWriter(bw);//完成简易记事本。控制台输入的每行字符串都按行写入文件。单独输入exit时退出。Scanner scanner = new Scanner(System.in);while(true){String line = scanner.nextLine();if("exit".equalsIgnoreCase(line)){break;}pw.println(line);}pw.close();}
}
PrintWriter的自动行刷新功能

如果实例化PW时第一个参数传入的是一个流,则此时可以再传入一个boolean型的参数,此值为true时就打开了自动行刷新功能。
即:
每当我们用PW的println方法写出一行字符串后会自动flush.

package io;import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;/*** 在流连接中使用PW*/
public class PWDemo2 {public static void main(String[] args) throws FileNotFoundException {//文件字节输出流(是一个低级流),向文件中写入字节数据FileOutputStream fos = new FileOutputStream("pw2.txt",true);//转换输出流(是一个高级流,且是一个字符流)。1:衔接字符与字节流 2:将写出的字符转换为字节OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8);//缓冲输出流(是一个高级流,且是一个字符流)。块写文本数据加速BufferedWriter bw = new BufferedWriter(osw);//具有自动行刷新的缓冲字符输出流/*PrintWriter提供的构造器中,当第一个参数为一个流时,就支持再传入一个boolean型的参数表示是否自动行刷新。当该值为true时则打开了自动行刷新功能。这意味着每当我们调用println方法后会自动flush一次。*/PrintWriter pw = new PrintWriter(bw,true);//完成简易记事本。控制台输入的每行字符串都按行写入文件。单独输入exit时退出。Scanner scanner = new Scanner(System.in);while(true){String line = scanner.nextLine();if("exit".equalsIgnoreCase(line)){break;}pw.println(line);}pw.close();}
}

缓冲字符流

缓冲字符输入流:java.io.BufferedReader

是一个高级的字符流,特点是块读文本数据,并且可以按行读取字符串。

package io;import java.io.*;/*** 使用java.io.BufferedReader按行读取文本数据*/
public class BRDemo {public static void main(String[] args) throws IOException {//将当前源程序读取出来并输出到控制台上FileInputStream fis = new FileInputStream("./src/io/BRDemo.java");InputStreamReader isr = new InputStreamReader(fis);BufferedReader br = new BufferedReader(isr);String line;/*BufferedReader提供了一个读取一行字符串的方法:String readLine()该方法会返回一行字符串,返回的字符串不含有最后的换行符。当某一行是空行时(该行内容只有一个换行符)则返回值为空字符串。如果流读取到了末尾,则返回值为null。*/while((line = br.readLine()) != null) {System.out.println(line);}br.close();}
}
IO总结(非常重要)

异常处理

java异常处理机制

  • java中所有错误的超类为:Throwable。其下有两个子类:Error和Exception
  • Error的子类描述的都是系统错误,比如虚拟机内存溢出等。
  • Exception的子类描述的都是程序错误,比如空指针,下表越界等。
  • 通常我们程序中处理的异常都是Exception。

异常处理机制中的try-catch

package exception;/*** 异常处理机制中的try-catch* 语法:* try{*     可能出现异常的代码片段* }catch(XXXException e){*     try中出现XXXException后的处理代码* }** try语句块不能独立存在,后面必须跟catch语句块或finally语句块*/
public class TryCatchDemo {public static void main(String[] args) {System.out.println("程序开始了");try {//            String line = null;
//            String line = "";String line = "abc";//当JVM执行程序出现了某个异常时就会实例化这个异常并将其抛出//如果该异常没有被异常处理机制控制,则JVM会将异常隐式抛出当方法外(这里是main方法外)System.out.println(line.length());System.out.println(line.charAt(0));System.out.println(Integer.parseInt(line));//若try语句块中某句话出错了,则剩下的代码都不会执行!System.out.println("!!!!!!!!!!!!!!!!");//        }catch(NullPointerException e){//            System.out.println("出现了空指针!");
//        //catch可以定义多个,当try中不同的异常有不同处理办法时可分开捕获并处理
//        }catch(StringIndexOutOfBoundsException e){//            System.out.println("出现了下标越界!");//若某些异常的处理方式相同时,可以合并在一个catch来处理}catch(NullPointerException|StringIndexOutOfBoundsException e){System.out.println("出现了空指针或下标越界并处理了!");//可以在下面catch超类异常来捕获并处理这一类异常。}catch(Exception e){System.out.println("反正就是出了个错");}System.out.println("程序结束了");}
}

异常处理机制中的finally

  • finally块定义在异常处理机制中的最后一块。它可以直接跟在try之后,或者最后一个catch之后。

  • finally可以保证只要程序执行到了try语句块中,无论try语句块中的代码是否出现异常,最终finally都必定执行。

  • finally通常用来做释放资源这类操作。

package exception;/*** 异常处理机制中的finally块* finally块定义在异常处理机制中的最后一块。它可以直接跟在try之后,或者最后一个catch之后。** finally可以保证只要程序执行到了try语句块中,无论try语句块中的代码是否出现异常,最终*  finally都必定执行。** finally通常用来做释放资源这类操作。*/
public class FinallyDemo {public static void main(String[] args) {System.out.println("程序开始了...");try{String line = "abc";
//            String line = null;System.out.println(line.length());return;}catch(Exception e){System.out.println("出错了!");}finally{System.out.println("finally中的代码执行了!");}System.out.println("程序结束了!");}
}

IO操作时的异常处理机制应用

package exception;import java.io.FileOutputStream;
import java.io.IOException;/*** IO操作时的异常处理机制应用*/
public class FinallyDemo2 {public static void main(String[] args) {FileOutputStream fos = null;try {fos = new FileOutputStream("fos.dat");fos.write(1);} catch (IOException e) {e.printStackTrace();//向控制台输出当前异常的错误信息} finally {try {if (fos!=null) {fos.close();}} catch (IOException e) {e.printStackTrace();}}}
}

自动关闭特性

JDK7之后,java提供了一个新的特性:自动关闭。旨在IO操作中可以更简洁的使用异常处理机制完成最后的close操作。

语法:
try(定义需要在finally中调用close()方法关闭的对象.
){IO操作
}catch(XXXException e){...
}

上述语法中可在try的"()"中定义的并初始化的对象必须实现了java.io.AutoCloseable接口,否则编译不通过.

public class AutocloseableDemo {public static void main(String[] args) {try(FileOutputStream fos = new FileOutputStream("fos.dat");){fos.write(1);} catch (IOException e) {e.printStackTrace();//向控制台输出当前异常的错误信息}}
}

上述代码是编译器认可的,而不是虚拟机。编译器在编译上述代码后会在编译后的class文件中改回成FinallyDemo2案例的代码样子(上次课最后的案例)。

throw关键字

throw用来对外主动抛出一个异常,通常下面两种情况我们主动对外抛出异常:

  • 1:当程序遇到一个满足语法,但是不满足业务要求时,可以抛出一个异常告知调用者。
  • 2:程序执行遇到一个异常,但是该异常不应当在当前代码片段被解决时可以抛出给调用者。
package exception;/*** 测试异常的抛出*/
public class Person {private int age;public int getAge() {return age;}public void setAge(int age) throws Exception {if(age<0||age>100){//使用throw对外抛出一个异常throw new RuntimeException("年龄不合法!");}this.age = age;}
}
package exception;/*** throw关键字,用来对外主动抛出一个异常。* 通常下面两种情况我们主动对外抛出异常:* 1:当程序遇到一个满足语法,但是不满足业务要求时,可以抛出一个异常告知调用者。* 2:程序执行遇到一个异常,但是该异常不应当在当前代码片段被解决时可以抛出给调用者。*/
public class ThrowDemo {public static void main(String[] args) {Person p = new Person();p.setAge(10000);//符合语法,但是不符合业务逻辑要求。System.out.println("此人年龄:"+p.getAge());}
}

throws关键字

当一个方法中使用throw抛出一个非RuntimeException的异常时,就要在该方法上使用throws声明这个异常的抛出。此时调用该方法的代码就必须处理这个异常,否则编译不通过。

package exception;/*** 测试异常的抛出*/
public class Person {private int age;public int getAge() {return age;}/*** 当一个方法使用throws声明异常抛出时,调用此方法的代码片段就必须处理这个异常*/public void setAge(int age) throws Exception {if(age<0||age>100){//使用throw对外抛出一个异常
//            throw new RuntimeException("年龄不合法!");//除了RuntimeException之外,抛出什么异常就要在方法上声明throws什么异常throw new Exception("年龄不合法!");}this.age = age;}
}

当我们调用一个含有throws声明异常抛出的方法时,编译器要求我们必须处理这个异常,否则编译不通过。 处理手段有两种:

  • 使用try-catch捕获并处理这个异常
  • 在当前方法(本案例就是main方法)上继续使用throws声明该异常的抛出给调用者解决。 具体选取那种取决于异常处理的责任问题。
package exception;/*** throw关键字,用于主动对外抛出一个异常*/
public class ThrowDemo {public static void main(String[] args){System.out.println("程序开始了...");try {Person p = new Person();/*当我们调用一个含有throws声明异常抛出的方法时,编译器要求我们必须添加处理异常的手段,否则编译不通过.而处理手段有两种1:使用try-catch捕获并处理异常2:在当前方法上继续使用throws声明该异常的抛出具体用哪种取决于异常处理的责任问题*/p.setAge(100000);//典型的符合语法,但是不符合业务逻辑要求System.out.println("此人年龄:"+p.getAge()+"岁");} catch (Exception e) {e.printStackTrace();}System.out.println("程序结束了...");}
}

注意,永远不应当在main方法上使用throws!!

含有throws的方法被子类重写时的规则

package exception;import java.awt.*;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.sql.SQLException;/*** 子类重写超类含有throws声明异常抛出的方法时对throws的几种特殊的重写规则*/
public class ThrowsDemo {public void dosome()throws IOException, AWTException {}
}
class SubClass extends ThrowsDemo{//    public void dosome()throws IOException, AWTException {}//可以不再抛出任何异常
//    public void dosome(){}//可以仅抛出部分异常
//    public void dosome()throws IOException {}//可以抛出超类方法抛出异常的子类型异常
//    public void dosome()throws FileNotFoundException {}//不允许抛出额外异常(超类方法中没有的,并且没有继承关系的异常)
//    public void dosome()throws SQLException {}//不可以抛出超类方法抛出异常的超类型异常
//    public void dosome()throws Exception {}
}

Java异常可以分为可检测异常,非检测异常:

  • 可检测异常:可检测异常经编译器验证,对于声明抛出异常的任何方法,编译器将强制执行处理或声明规则,不捕捉这个异常,编译器就通不过,不允许编译
  • 非检测异常:非检测异常不遵循处理或者声明规则。在产生此类异常时,不一定非要采取任何适当操作,编译器不会检查是否已经解决了这样一个异常
  • RuntimeException 类属于非检测异常,因为普通JVM操作引起的运行时异常随时可能发生,此类异常一般是由特定操作引发。但这些操作在java应用程序中会频繁出现。因此它们不受编译器检查与处理或声明规则的限制。

常见的RuntimeException子类

  • IllegalArgumentException:抛出的异常表明向方法传递了一个不合法或不正确的参数
  • NullPointerException:当应用程序试图在需要对象的地方使用 null 时,抛出该异常
  • ArrayIndexOutOfBoundsException:当使用的数组下标超出数组允许范围时,抛出该异常
  • ClassCastException:当试图将对象强制转换为不是实例的子类时,抛出该异常
  • NumberFormatException:当应用程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常。

异常中常用的方法

package exception;/*** 异常常见的方法*/
public class ExceptionApiDemo {public static void main(String[] args) {System.out.println("程序开始了");try {String str = "abc";System.out.println(Integer.parseInt(str));} catch (NumberFormatException e) {//异常最常用的方法,用于将当前错误信息输出到控制台e.printStackTrace();//获取错误消息.记录日志的时候或提示给用户可以使用它String message = e.getMessage();System.out.println(message);}System.out.println("程序结束了");}
}

自定义异常

自定义异常通常用来定义那些业务上的异常问题。

定义自定义异常需要注意以下问题:

  • 异常的类名要做到见名知义
  • 需要是Exception的子类
  • 提供超类异常提供的所有种类构造器
package exception;/*** 非法的年龄异常** 自定义异常通常用来说明业务上的错误.* 自定义异常要注意以下问题:* 1:定义的类名要做到见名知义* 2:必须是Exception的子类* 3:提供Exception所定义的所有构造方法*/
public class IllegalAgeException extends Exception{public IllegalAgeException() {}public IllegalAgeException(String message) {super(message);}public IllegalAgeException(String message, Throwable cause) {super(message, cause);}public IllegalAgeException(Throwable cause) {super(cause);}public IllegalAgeException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {super(message, cause, enableSuppression, writableStackTrace);}
}
package exception;/*** 测试异常的抛出*/
public class Person {private int age;public int getAge() {return age;}/*** 当一个方法使用throws声明异常抛出时,调用此方法的代码片段就必须处理这个异常*/public void setAge(int age) throws IllegalAgeException {if(age<0||age>100){//使用throw对外抛出一个异常
//            throw new RuntimeException("年龄不合法!");//除了RuntimeException之外,抛出什么异常就要在方法上声明throws什么异常
//            throw new Exception("年龄不合法!");//抛出自定义异常throw new IllegalAgeException("年龄超范围:"+age);}this.age = age;}
}
package exception;/*** throw关键字,用于主动对外抛出一个异常*/
public class ThrowDemo {public static void main(String[] args){System.out.println("程序开始了...");try {Person p = new Person();/*当我们调用一个含有throws声明异常抛出的方法时,编译器要求我们必须添加处理异常的手段,否则编译不通过.而处理手段有两种1:使用try-catch捕获并处理异常2:在当前方法上继续使用throws声明该异常的抛出具体用哪种取决于异常处理的责任问题*/p.setAge(100000);//典型的符合语法,但是不符合业务逻辑要求System.out.println("此人年龄:"+p.getAge()+"岁");} catch (IllegalAgeException e) {e.printStackTrace();}System.out.println("程序结束了...");}
}

总结:

异常处理机制是用来处理那些可能存在的异常,但是无法通过修改逻辑完全规避的场景。

而如果通过修改逻辑可以规避的异常是bug,不应当用异常处理机制在运行期间解决!应当在编码时及时修正

java网络编程

注:下面是一个聊天室项目

java.net.Socket

Socket(套接字)封装了TCP协议的通讯细节,是的我们使用它可以与服务端建立网络链接,并通过 它获取两个流(一个输入一个输出),然后使用这两个流的读写操作完成与服务端的数据交互

java.net.ServerSocket

ServerSocket运行在服务端,作用有两个:

1:向系统申请服务端口,客户端的Socket就是通过这个端口与服务端建立连接的。

2:监听服务端口,一旦一个客户端通过该端口建立连接则会自动创建一个Socket,并通过该Socket与客户端进行数据交互。

如果我们把Socket比喻为电话,那么ServerSocket相当于是某客服中心的总机。
与服务端建立连接案例:
package socket;import java.io.IOException;
import java.net.Socket;/*** 聊天室客户端*/
public class Client {/*java.net.Socket 套接字Socket封装了TCP协议的通讯细节,我们通过它可以与远端计算机建立链接,并通过它获取两个流(一个输入,一个输出),然后对两个流的数据读写完成与远端计算机的数据交互工作。我们可以把Socket想象成是一个电话,电话有一个听筒(输入流),一个麦克风(输出流),通过它们就可以与对方交流了。*/private Socket socket;/*** 构造方法,用来初始化客户端*/public Client(){try {System.out.println("正在链接服务端...");/*实例化Socket时要传入两个参数参数1:服务端的地址信息可以是IP地址,如果链接本机可以写"localhost"参数2:服务端开启的服务端口我们通过IP找到网络上的服务端计算机,通过端口链接运行在该机器上的服务端应用程序。实例化的过程就是链接的过程,如果链接失败会抛出异常:java.net.ConnectException: Connection refused: connect*/socket = new Socket("localhost",8088);System.out.println("与服务端建立链接!");} catch (IOException e) {e.printStackTrace();}}/*** 客户端开始工作的方法*/public void start(){}public static void main(String[] args) {Client client = new Client();client.start();}
}
package socket;import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;/*** 聊天室服务端*/
public class Server {/*** 运行在服务端的ServerSocket主要完成两个工作:* 1:向服务端操作系统申请服务端口,客户端就是通过这个端口与ServerSocket建立链接* 2:监听端口,一旦一个客户端建立链接,会立即返回一个Socket。通过这个Socket*   就可以和该客户端交互了** 我们可以把ServerSocket想象成某客服的"总机"。用户打电话到总机,总机分配一个* 电话使得服务端与你沟通。*/private ServerSocket serverSocket;/*** 服务端构造方法,用来初始化*/public Server(){try {System.out.println("正在启动服务端...");/*实例化ServerSocket时要指定服务端口,该端口不能与操作系统其他应用程序占用的端口相同,否则会抛出异常:java.net.BindException:address already in use端口是一个数字,取值范围:0-65535之间。6000之前的的端口不要使用,密集绑定系统应用和流行应用程序。*/serverSocket = new ServerSocket(8088);System.out.println("服务端启动完毕!");} catch (IOException e) {e.printStackTrace();}}/*** 服务端开始工作的方法*/public void start(){try {System.out.println("等待客户端链接...");/*ServerSocket提供了接受客户端链接的方法:Socket accept()这个方法是一个阻塞方法,调用后方法"卡住",此时开始等待客户端的链接,直到一个客户端链接,此时该方法会立即返回一个Socket实例通过这个Socket就可以与客户端进行交互了。可以理解为此操作是接电话,电话没响时就一直等。*/Socket socket = serverSocket.accept();System.out.println("一个客户端链接了!");} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) {Server server = new Server();server.start();}
}

客户端与服务端完成第一次通讯(发送一行字符串)

Socket提供了两个重要的方法:

OutputStream getOutputStream()

该方法会获取一个字节输出流,通过这个输出流写出的字节数据会通过网络发送给对方。

InputStream getInputStream()

通过该方法获取的字节输入流读取的是远端计算机发送过来的数据。

客户端代码:

package socket;import java.io.*;
import java.net.Socket;/*** 聊天室客户端*/
public class Client {/*java.net.Socket 套接字Socket封装了TCP协议的通讯细节,我们通过它可以与远端计算机建立链接,并通过它获取两个流(一个输入,一个输出),然后对两个流的数据读写完成与远端计算机的数据交互工作。我们可以把Socket想象成是一个电话,电话有一个听筒(输入流),一个麦克风(输出流),通过它们就可以与对方交流了。*/private Socket socket;/*** 构造方法,用来初始化客户端*/public Client(){try {System.out.println("正在链接服务端...");/*实例化Socket时要传入两个参数参数1:服务端的地址信息可以是IP地址,如果链接本机可以写"localhost"参数2:服务端开启的服务端口我们通过IP找到网络上的服务端计算机,通过端口链接运行在该机器上的服务端应用程序。实例化的过程就是链接的过程,如果链接失败会抛出异常:java.net.ConnectException: Connection refused: connect*/socket = new Socket("localhost",8088);System.out.println("与服务端建立链接!");} catch (IOException e) {e.printStackTrace();}}/*** 客户端开始工作的方法*/public void start(){try {/*Socket提供了一个方法:OutputStream getOutputStream()该方法获取的字节输出流写出的字节会通过网络发送给对方计算机。*///低级流,将字节通过网络发送给对方OutputStream out = socket.getOutputStream();//高级流,负责衔接字节流与字符流,并将写出的字符按指定字符集转字节OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8");//高级流,负责块写文本数据加速BufferedWriter bw = new BufferedWriter(osw);//高级流,负责按行写出字符串,自动行刷新PrintWriter pw = new PrintWriter(bw,true);pw.println("你好服务端!");} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) {Client client = new Client();client.start();}
}

服务端代码:

package socket;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;/*** 聊天室服务端*/
public class Server {/*** 运行在服务端的ServerSocket主要完成两个工作:* 1:向服务端操作系统申请服务端口,客户端就是通过这个端口与ServerSocket建立链接* 2:监听端口,一旦一个客户端建立链接,会立即返回一个Socket。通过这个Socket*   就可以和该客户端交互了** 我们可以把ServerSocket想象成某客服的"总机"。用户打电话到总机,总机分配一个* 电话使得服务端与你沟通。*/private ServerSocket serverSocket;/*** 服务端构造方法,用来初始化*/public Server(){try {System.out.println("正在启动服务端...");/*实例化ServerSocket时要指定服务端口,该端口不能与操作系统其他应用程序占用的端口相同,否则会抛出异常:java.net.BindException:address already in use端口是一个数字,取值范围:0-65535之间。6000之前的的端口不要使用,密集绑定系统应用和流行应用程序。*/serverSocket = new ServerSocket(8088);System.out.println("服务端启动完毕!");} catch (IOException e) {e.printStackTrace();}}/*** 服务端开始工作的方法*/public void start(){try {System.out.println("等待客户端链接...");/*ServerSocket提供了接受客户端链接的方法:Socket accept()这个方法是一个阻塞方法,调用后方法"卡住",此时开始等待客户端的链接,直到一个客户端链接,此时该方法会立即返回一个Socket实例通过这个Socket就可以与客户端进行交互了。可以理解为此操作是接电话,电话没响时就一直等。*/Socket socket = serverSocket.accept();System.out.println("一个客户端链接了!");/*Socket提供的方法:InputStream getInputStream()获取的字节输入流读取的是对方计算机发送过来的字节*/InputStream in = socket.getInputStream();InputStreamReader isr = new InputStreamReader(in,"UTF-8");BufferedReader br = new BufferedReader(isr);String message = br.readLine();System.out.println("客户端说:"+message);} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) {Server server = new Server();server.start();}
}

实现客户端循环发消息给服务端

客户端代码:

package socket;import java.io.*;
import java.net.Socket;
import java.util.Scanner;/*** 聊天室客户端*/
public class Client {/*java.net.Socket 套接字Socket封装了TCP协议的通讯细节,我们通过它可以与远端计算机建立链接,并通过它获取两个流(一个输入,一个输出),然后对两个流的数据读写完成与远端计算机的数据交互工作。我们可以把Socket想象成是一个电话,电话有一个听筒(输入流),一个麦克风(输出流),通过它们就可以与对方交流了。*/private Socket socket;/*** 构造方法,用来初始化客户端*/public Client(){try {System.out.println("正在链接服务端...");/*实例化Socket时要传入两个参数参数1:服务端的地址信息可以是IP地址,如果链接本机可以写"localhost"参数2:服务端开启的服务端口我们通过IP找到网络上的服务端计算机,通过端口链接运行在该机器上的服务端应用程序。实例化的过程就是链接的过程,如果链接失败会抛出异常:java.net.ConnectException: Connection refused: connect*/socket = new Socket("localhost",8088);System.out.println("与服务端建立链接!");} catch (IOException e) {e.printStackTrace();}}/*** 客户端开始工作的方法*/public void start(){try {/*Socket提供了一个方法:OutputStream getOutputStream()该方法获取的字节输出流写出的字节会通过网络发送给对方计算机。*///低级流,将字节通过网络发送给对方OutputStream out = socket.getOutputStream();//高级流,负责衔接字节流与字符流,并将写出的字符按指定字符集转字节OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8");//高级流,负责块写文本数据加速BufferedWriter bw = new BufferedWriter(osw);//高级流,负责按行写出字符串,自动行刷新PrintWriter pw = new PrintWriter(bw,true);Scanner scanner = new Scanner(System.in);while(true) {String line = scanner.nextLine();if("exit".equalsIgnoreCase(line)){break;}pw.println(line);}} catch (IOException e) {e.printStackTrace();} finally {try {/*通讯完毕后调用socket的close方法。该方法会给对方发送断开信号。*/socket.close();} catch (IOException e) {e.printStackTrace();}}}public static void main(String[] args) {Client client = new Client();client.start();}
}

服务端代码:

package socket;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;/*** 聊天室服务端*/
public class Server {/*** 运行在服务端的ServerSocket主要完成两个工作:* 1:向服务端操作系统申请服务端口,客户端就是通过这个端口与ServerSocket建立链接* 2:监听端口,一旦一个客户端建立链接,会立即返回一个Socket。通过这个Socket*   就可以和该客户端交互了** 我们可以把ServerSocket想象成某客服的"总机"。用户打电话到总机,总机分配一个* 电话使得服务端与你沟通。*/private ServerSocket serverSocket;/*** 服务端构造方法,用来初始化*/public Server(){try {System.out.println("正在启动服务端...");/*实例化ServerSocket时要指定服务端口,该端口不能与操作系统其他应用程序占用的端口相同,否则会抛出异常:java.net.BindException:address already in use端口是一个数字,取值范围:0-65535之间。6000之前的的端口不要使用,密集绑定系统应用和流行应用程序。*/serverSocket = new ServerSocket(8088);System.out.println("服务端启动完毕!");} catch (IOException e) {e.printStackTrace();}}/*** 服务端开始工作的方法*/public void start(){try {System.out.println("等待客户端链接...");/*ServerSocket提供了接受客户端链接的方法:Socket accept()这个方法是一个阻塞方法,调用后方法"卡住",此时开始等待客户端的链接,直到一个客户端链接,此时该方法会立即返回一个Socket实例通过这个Socket就可以与客户端进行交互了。可以理解为此操作是接电话,电话没响时就一直等。*/Socket socket = serverSocket.accept();System.out.println("一个客户端链接了!");/*Socket提供的方法:InputStream getInputStream()获取的字节输入流读取的是对方计算机发送过来的字节*/InputStream in = socket.getInputStream();InputStreamReader isr = new InputStreamReader(in,"UTF-8");BufferedReader br = new BufferedReader(isr);String message = null;while((message = br.readLine())!=null) {System.out.println("客户端说:" + message);}} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) {Server server = new Server();server.start();}
}

需要注意的几个点:

1:当客户端不再与服务端通讯时,需要调用socket.close()断开链接,此时会发送断开链接的信号给服务端。这时服务端的br.readLine()方法会返回null,表示客户端断开了链接。

2:当客户端链接后不输入信息发送给服务端时,服务端的br.readLine()方法是出于阻塞状态的,直到读取了一行来自客户端发送的字符串。

多客户端链接

之前只有第一个连接的客户端可以与服务端说话。

原因:

服务端只调用过一次accept方法,因此只有第一个客户端链接时服务端接受了链接并返回了Socket,此时可以与其交互。

而第二个客户端建立链接时,由于服务端没有再次调用accept,因此无法与其交互。

package socket;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;/*** 聊天室服务端*/
public class Server {/*** 运行在服务端的ServerSocket主要完成两个工作:* 1:向服务端操作系统申请服务端口,客户端就是通过这个端口与ServerSocket建立链接* 2:监听端口,一旦一个客户端建立链接,会立即返回一个Socket。通过这个Socket*   就可以和该客户端交互了** 我们可以把ServerSocket想象成某客服的"总机"。用户打电话到总机,总机分配一个* 电话使得服务端与你沟通。*/private ServerSocket serverSocket;/*** 服务端构造方法,用来初始化*/public Server(){try {System.out.println("正在启动服务端...");/*实例化ServerSocket时要指定服务端口,该端口不能与操作系统其他应用程序占用的端口相同,否则会抛出异常:java.net.BindException:address already in use端口是一个数字,取值范围:0-65535之间。6000之前的的端口不要使用,密集绑定系统应用和流行应用程序。*/serverSocket = new ServerSocket(8088);System.out.println("服务端启动完毕!");} catch (IOException e) {e.printStackTrace();}}/*** 服务端开始工作的方法*/public void start(){try {while(true) {System.out.println("等待客户端链接...");/*ServerSocket提供了接受客户端链接的方法:Socket accept()这个方法是一个阻塞方法,调用后方法"卡住",此时开始等待客户端的链接,直到一个客户端链接,此时该方法会立即返回一个Socket实例通过这个Socket就可以与客户端进行交互了。可以理解为此操作是接电话,电话没响时就一直等。*/Socket socket = serverSocket.accept();System.out.println("一个客户端链接了!");/*Socket提供的方法:InputStream getInputStream()获取的字节输入流读取的是对方计算机发送过来的字节*/InputStream in = socket.getInputStream();InputStreamReader isr = new InputStreamReader(in, "UTF-8");BufferedReader br = new BufferedReader(isr);String message = null;while ((message = br.readLine()) != null) {System.out.println("客户端说:" + message);}}} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) {Server server = new Server();server.start();}
}

添加循环操作后,发现依然无法实现。

原因在于:

外层的while循环里面嵌套了一个内层循环(循环读取客户端发送消息),而循环执行机制决定了里层循环不结束,外层循环则无法进入第二次操作。

多线程

线程:一个顺序的单一的程序执行流程就是一个线程。代码一句一句的有先后顺序的执行。
多线程:多个单一顺序执行的流程并发运行。造成"感官上同时运行"的效果。
并发:

多个线程实际运行是走走停停的。线程调度程序会将CPU运行时间划分为若干个时间片段并

尽可能均匀的分配给每个线程,拿到时间片的线程被CPU执行这段时间。当超时后线程调度

程序会再次分配一个时间片段给一个线程使得CPU执行它。如此反复。由于CPU执行时间在

纳秒级别,我们感觉不到切换线程运行的过程。所以微观上走走停停,宏观上感觉一起运行

的现象成为并发运行!

用途:
  • 当出现多个代码片段执行顺序有冲突时,希望它们各干各的时就应当放在不同线程上"同时"运行
  • 一个线程可以运行,但是多个线程可以更快时,可以使用多线程运行
线程的生命周期图

创建线程有两种方式

方式一:继承Thread并重写run方法

定义一个线程类,重写run方法,在其中定义线程要执行的任务(希望和其他线程并发执行的任务)。

注:启动该线程要调用该线程的start方法,而不是run方法!!!

package thread;/*** 多线程* 线程:程序中一个单一的顺序执行流程* 多线程:多个单一顺序执行流程"同时"执行** 多线程改变了代码的执行方式,从原来的单一顺序执行流程变为多个执行流程"同时"执行。* 可以让多个代码片段的执行互不打扰。** 线程之间是并发执行的,并非真正意义上的同时运行。* 常见线程有两种方式:* 1:继承Thread并重写run方法**/
public class ThreadDemo1 {public static void main(String[] args) {//创建两个线程Thread t1 = new MyThread1();Thread t2 = new MyThread2();/*启动线程,注意:不要调用run方法!!线程调用完start方法后会纳入到系统的线程调度器程序中被统一管理。线程调度器会分配时间片段给线程,使得CPU执行该线程这段时间,用完后线程调度器会再分配一个时间片段给一个线程,如此反复,使得多个线程都有机会执行一会,做到走走停停,并发运行。线程第一次被分配到时间后会执行它的run方法开始工作。*/t1.start();t2.start();}
}
/*** 第一种创建线程的优点:* 结构简单,利于匿名内部类形式创建。** 缺点:* 1:由于java是单继承的,这会导致继承了Thread就无法再继承其他类去复用方法* 2:定义线程的同时重写了run方法,这等于将线程的任务定义在了这个线程中导致*   线程只能干这件事。重(chong)用性很低。*/
class MyThread1 extends Thread{public void run(){for (int i=0;i<1000;i++){System.out.println("hello姐~");}}
}
class MyThread2 extends Thread{public void run(){for (int i=0;i<1000;i++){System.out.println("来了~老弟!");}}
}

第一种创建线程的方式

优点:

在于结构简单,便于匿名内部类形式创建。

缺点:

  • 1:直接继承线程,会导致不能在继承其他类去复用方法,这在实际开发中是非常不便的。
  • 2:定义线程的同时重写了run方法,会导致线程与线程任务绑定在了一起,不利于线程的重用。
方式二:实现Runnable接口单独定义线程任务
package thread;/*** 第二种创建线程的方式* 实现Runnable接口单独定义线程任务*/
public class ThreadDemo2 {public static void main(String[] args) {//实例化任务Runnable r1 = new MyRunnable1();Runnable r2 = new MyRunnable2();//创建线程并指派任务Thread t1 = new Thread(r1);Thread t2 = new Thread(r2);t1.start();t2.start();}
}
class MyRunnable1 implements Runnable{public void run() {for (int i=0;i<1000;i++){System.out.println("你是谁啊?");}}
}
class MyRunnable2 implements Runnable{public void run() {for (int i=0;i<1000;i++){System.out.println("开门!查水表的!");}}
}

匿名内部类形式的线程创建

package thread;/*** 使用匿名内部类完成线程的两种创建*/
public class ThreadDemo3 {public static void main(String[] args) {Thread t1 = new Thread(){public void run(){for(int i=0;i<1000;i++){System.out.println("你是谁啊?");}}};
//        Runnable r2 = new Runnable() {//            public void run() {//                for(int i=0;i<1000;i++){//                    System.out.println("我是查水表的!");
//                }
//            }
//        };//Runnable可以使用lambda表达式创建Runnable r2 = ()->{for(int i=0;i<1000;i++){System.out.println("我是查水表的!");}};Thread t2 = new Thread(r2);t1.start();t2.start();}
}

java中的代码都是靠线程运行的,执行main方法的线程称为"主线程"。

线程提供了一个方法:

  • static Thread currentThread()

    该方法可以获取运行这个方法的线程

package thread;/*** java中所有的代码都是靠线程执行的,main方法也不例外。JVM启动后会创建一条线程来执行main* 方法,该线程的名字叫做"main",所以通常称它为"主线程"。* 我们自己定义的线程在不指定名字的情况下系统会分配一个名字,格式为"thread-x"(x是一个数)。** Thread提供了一个静态方法:* static Thread currentThread()* 获取执行该方法的线程。**/
public class CurrentThreadDemo {public static void main(String[] args) {/*后期会学习到一个很重要的API:ThreadLocal,它可以使得我们在一个线程上跨越多个方法时共享数据使用,其内部要用到currentThread方法来辨别线程。如spring的事物控制就是靠ThreadLocal实现的。*/Thread main = Thread.currentThread();//获取执行main方法的线程(主线程)System.out.println("线程:"+main);dosome();//主线程执行dosome方法}public static void dosome(){Thread t = Thread.currentThread();//获取执行dosome方法的线程System.out.println("执行dosome方法的线程是:"+t);}
}

使用多线程实现多客户端连接服务端

流程图

服务端代码改造:

package socket;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;/*** 聊天室服务端*/
public class Server {/*** 运行在服务端的ServerSocket主要完成两个工作:* 1:向服务端操作系统申请服务端口,客户端就是通过这个端口与ServerSocket建立链接* 2:监听端口,一旦一个客户端建立链接,会立即返回一个Socket。通过这个Socket*   就可以和该客户端交互了** 我们可以把ServerSocket想象成某客服的"总机"。用户打电话到总机,总机分配一个* 电话使得服务端与你沟通。*/private ServerSocket serverSocket;/*** 服务端构造方法,用来初始化*/public Server(){try {System.out.println("正在启动服务端...");/*实例化ServerSocket时要指定服务端口,该端口不能与操作系统其他应用程序占用的端口相同,否则会抛出异常:java.net.BindException:address already in use端口是一个数字,取值范围:0-65535之间。6000之前的的端口不要使用,密集绑定系统应用和流行应用程序。*/serverSocket = new ServerSocket(8088);System.out.println("服务端启动完毕!");} catch (IOException e) {e.printStackTrace();}}/*** 服务端开始工作的方法*/public void start(){try {while(true) {System.out.println("等待客户端链接...");/*ServerSocket提供了接受客户端链接的方法:Socket accept()这个方法是一个阻塞方法,调用后方法"卡住",此时开始等待客户端的链接,直到一个客户端链接,此时该方法会立即返回一个Socket实例通过这个Socket就可以与客户端进行交互了。可以理解为此操作是接电话,电话没响时就一直等。*/Socket socket = serverSocket.accept();System.out.println("一个客户端链接了!");//启动一个线程与该客户端交互ClientHandler clientHandler = new ClientHandler(socket);Thread t = new Thread(clientHandler);t.start();}} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) {Server server = new Server();server.start();}/*** 定义线程任务* 目的是让一个线程完成与特定客户端的交互工作*/private class ClientHandler implements Runnable{private Socket socket;public ClientHandler(Socket socket){this.socket = socket;}public void run(){try{/*Socket提供的方法:InputStream getInputStream()获取的字节输入流读取的是对方计算机发送过来的字节*/InputStream in = socket.getInputStream();InputStreamReader isr = new InputStreamReader(in, "UTF-8");BufferedReader br = new BufferedReader(isr);String message = null;while ((message = br.readLine()) != null) {System.out.println("客户端说:" + message);}}catch(IOException e){e.printStackTrace();}}}}

线程API

获取线程相关信息的方法

package thread;/*** 获取线程相关信息的一组方法*/
public class ThreadInfoDemo {public static void main(String[] args) {Thread main = Thread.currentThread();//获取主线程String name = main.getName();//获取线程的名字System.out.println("名字:"+name);long id = main.getId();//获取该线程的唯一标识System.out.println("id:"+id);int priority = main.getPriority();//获取该线程的优先级System.out.println("优先级:"+priority);boolean isAlive = main.isAlive();//该线程是否活着System.out.println("是否活着:"+isAlive);boolean isDaemon = main.isDaemon();//是否为守护线程System.out.println("是否为守护线程:"+isDaemon);boolean isInterrupted = main.isInterrupted();//是否被中断了System.out.println("是否被中断了:"+isInterrupted);}
}
线程优先级

线程start后会纳入到线程调度器中统一管理,线程只能被动的被分配时间片并发运行,而无法主动索取时间片.线程调度器尽可能均匀的将时间片分配给每个线程.

线程有10个优先级,使用整数1-10表示

  • 1为最小优先级,10为最高优先级.5为默认值
  • 调整线程的优先级可以最大程度的干涉获取时间片的几率.优先级越高的线程获取时间片的次数越多,反之则越少.
package thread;public class PriorityDemo {public static void main(String[] args) {Thread max = new Thread(){public void run(){for(int i=0;i<10000;i++){System.out.println("max");}}};Thread min = new Thread(){public void run(){for(int i=0;i<10000;i++){System.out.println("min");}}};Thread norm = new Thread(){public void run(){for(int i=0;i<10000;i++){System.out.println("nor");}}};min.setPriority(Thread.MIN_PRIORITY);max.setPriority(Thread.MAX_PRIORITY);min.start();norm.start();max.start();}
}
sleep阻塞

线程提供了一个静态方法:

  • static void sleep(long ms)
  • 使运行该方法的线程进入阻塞状态指定的毫秒,超时后线程会自动回到RUNNABLE状态等待再次获取时间片并发运行.
package thread;public class SleepDemo {public static void main(String[] args) {System.out.println("程序开始了!");try {Thread.sleep(5000);//主线程阻塞5秒钟} catch (InterruptedException e) {e.printStackTrace();}System.out.println("程序结束了!");}
}

sleep方法处理异常:InterruptedException.

当一个线程调用sleep方法处于睡眠阻塞的过程中,该线程的interrupt()方法被调用时,sleep方法会抛出该异常从而打断睡眠阻塞.

package thread;/*** sleep方法要求必须处理中断异常:InterruptedException* 当一个线程调用sleep方法处于睡眠阻塞的过程中,它的interrupt()方法被调用时* 会中断该阻塞,此时sleep方法会抛出该异常。*/
public class SleepDemo2 {public static void main(String[] args) {Thread lin = new Thread(){public void run(){System.out.println("林:刚美完容,睡一会吧~");try {Thread.sleep(9999999);} catch (InterruptedException e) {System.out.println("林:干嘛呢!干嘛呢!干嘛呢!都破了像了!");}System.out.println("林:醒了");}};Thread huang = new Thread(){public void run(){System.out.println("黄:大锤80!小锤40!开始砸墙!");for(int i=0;i<5;i++){System.out.println("黄:80!");try {Thread.sleep(1000);} catch (InterruptedException e) {}}System.out.println("咣当!");System.out.println("黄:大哥,搞定!");lin.interrupt();//中断lin的睡眠阻塞}};lin.start();huang.start();}
}
守护线程

守护线程也称为:后台线程

  • 守护线程是通过普通线程调用setDaemon(boolean on)方法设置而来的,因此创建上与普通线程无异.
  • 守护线程的结束时机上有一点与普通线程不同,即:进程的结束.
  • 进程结束:当一个进程中的所有普通线程都结束时,进程就会结束,此时会杀掉所有正在运行的守护线程.
package thread;/*** 守护线程* 守护线程是通过普通线程调用setDaemon(true)设置而转变的。因此守护线程创建上* 与普通线程无异。* 但是结束时机上有一点不同:进程结束。* 当一个java进程中的所有普通线程都结束时,该进程就会结束,此时会强制杀死所有正在* 运行的守护线程。*/
public class DaemonThreadDemo {public static void main(String[] args) {Thread rose = new Thread(){public void run(){for(int i=0;i<5;i++){System.out.println("rose:let me go!");try {Thread.sleep(1000);} catch (InterruptedException e) {}}System.out.println("rose:啊啊啊啊啊啊AAAAAAAaaaaa....");System.out.println("噗通");}};Thread jack = new Thread(){public void run(){while(true){System.out.println("jack:you jump!i jump!");try {Thread.sleep(1000);} catch (InterruptedException e) {}}}};rose.start();jack.setDaemon(true);//设置守护线程必须在线程启动前进行jack.start();}
}

通常当我们不关心某个线程的任务什么时候停下来,它可以一直运行,但是程序主要的工作都结束时它应当跟着结束时,这样的任务就适合放在守护线程上执行.比如GC就是在守护线程上运行的.

join方法

线程提供了一个方法:void join()

  • 该方法允许调用这个方法的线程在该方法所属线程上等待(阻塞),直到该方法所属线程结束后才会解除等待继续后续的工作.所以join方法可以用来协调线程的同步运行.
  • 同步运行:多个线程执行过程存在先后顺序进行.
  • 异步运行:多个线程各干各的.线程本来就是异步运行的.
package thread;/*** 线程提供的方法:join可以协调线程的同步运行* 多线程是并发运行,本身是一种异步运行的状态。* 同步运行:多个线程执行是存在了先后顺序。* 异步运行:各自执行各自的** join:加入* finish:完成*/
public class JoinDemo {static boolean isFinish = false;//表示图片是否下载完毕public static void main(String[] args) {Thread download = new Thread(){public void run(){System.out.println("down:开始下载图片...");for(int i=1;i<=100;i++){System.out.println("down:"+i+"%");try {Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("down:图片下载完毕!");isFinish = true;}};Thread show = new Thread(){public void run(){System.out.println("show:开始显示文字");try {Thread.sleep(2000);} catch (InterruptedException e) {}System.out.println("show:显示文字完毕!");//将show线程阻塞,直到download执行完毕(图片下载完毕)
//                Thread.sleep(?);//无法准确预估download结束时间try {/*当show线程调用download.join()后便进入了阻塞状态。直到download线程执行完毕才会解除阻塞。*/download.join();} catch (InterruptedException e) {}System.out.println("show:开始显示图片...");if(!isFinish){throw new RuntimeException("图片加载失败!");}System.out.println("show:图片显示完毕");}};download.start();show.start();}
}

多线程并发安全问题

当多个线程并发操作同一临界资源,由于线程切换时机不确定,导致操作临界资源的顺序出现混乱严重时可能导致系统瘫痪.
临界资源:操作该资源的全过程同时只能被单个线程完成.
package thread;/*** 多线程并发安全问题* 当多个线程并发操作同一临界资源,由于线程切换的时机不确定,导致操作顺序出现* 混乱,严重时可能导致系统瘫痪。* 临界资源:同时只能被单一线程访问操作过程的资源。*/
public class SyncDemo {public static void main(String[] args) {Table table = new Table();Thread t1 = new Thread(){public void run(){while(true){int bean = table.getBean();Thread.yield();System.out.println(getName()+":"+bean);}}};Thread t2 = new Thread(){public void run(){while(true){int bean = table.getBean();/*static void yield()线程提供的这个静态方法作用是让执行该方法的线程主动放弃本次时间片。这里使用它的目的是模拟执行到这里CPU没有时间了,发生线程切换,来看并发安全问题的产生。*/Thread.yield();System.out.println(getName()+":"+bean);}}};t1.start();t2.start();}
}class Table{private int beans = 20;//桌子上有20个豆子public int getBean(){if(beans==0){throw new RuntimeException("没有豆子了!");}Thread.yield();return beans--;}
}

synchronized关键字

synchronized有两种使用方式
  • 在方法上修饰,此时该方法变为一个同步方法
  • 同步块,可以更准确的锁定需要排队的代码片段
同步方法

当一个方法使用synchronized修饰后,这个方法称为"同步方法",即:多个线程不能同时 在方法内部执行.只能有先后顺序的一个一个进行. 将并发操作同一临界资源的过程改为同步执行就可以有效的解决并发安全问题.

package thread;/*** 多线程并发安全问题* 当多个线程并发操作同一临界资源,由于线程切换的时机不确定,导致操作顺序出现* 混乱,严重时可能导致系统瘫痪。* 临界资源:同时只能被单一线程访问操作过程的资源。*/
public class SyncDemo {public static void main(String[] args) {Table table = new Table();Thread t1 = new Thread(){public void run(){while(true){int bean = table.getBean();Thread.yield();System.out.println(getName()+":"+bean);}}};Thread t2 = new Thread(){public void run(){while(true){int bean = table.getBean();/*static void yield()线程提供的这个静态方法作用是让执行该方法的线程主动放弃本次时间片。这里使用它的目的是模拟执行到这里CPU没有时间了,发生线程切换,来看并发安全问题的产生。*/Thread.yield();System.out.println(getName()+":"+bean);}}};t1.start();t2.start();}
}class Table{private int beans = 20;//桌子上有20个豆子/*** 当一个方法使用synchronized修饰后,这个方法称为同步方法,多个线程不能* 同时执行该方法。* 将多个线程并发操作临界资源的过程改为同步操作就可以有效的解决多线程并发* 安全问题。* 相当于让多个线程从原来的抢着操作改为排队操作。*/public synchronized int getBean(){if(beans==0){throw new RuntimeException("没有豆子了!");}Thread.yield();return beans--;}
}
同步块

有效的缩小同步范围可以在保证并发安全的前提下尽可能的提高并发效率.同步块可以更准确的控制需要多个线程排队执行的代码片段.

语法:

synchronized(同步监视器对象){需要多线程同步执行的代码片段
}
同步监视器对象即上锁的对象,要想保证同步块中的代码被多个线程同步运行,则要求多个线程看到的同步监视器对象是同一个.
package thread;/*** 有效的缩小同步范围可以在保证并发安全的前提下尽可能提高并发效率。** 同步块* 语法:* synchronized(同步监视器对象){*     需要多个线程同步执行的代码片段* }* 同步块可以更准确的锁定需要多个线程同步执行的代码片段来有效缩小排队范围。*/
public class SyncDemo2 {public static void main(String[] args) {Shop shop = new Shop();Thread t1 = new Thread(){public void run(){shop.buy();}};Thread t2 = new Thread(){public void run(){shop.buy();}};t1.start();t2.start();}
}class Shop{public void buy(){/*在方法上使用synchronized,那么同步监视器对象就是this。*/
//    public synchronized void buy(){Thread t = Thread.currentThread();//获取运行该方法的线程try {System.out.println(t.getName()+":正在挑衣服...");Thread.sleep(5000);/*使用同步块需要指定同步监视器对象,即:上锁的对象这个对象可以是java中任何引用类型的实例,只要保证多个需要排队执行该同步块中代码的线程看到的该对象是"同一个"即可*/synchronized (this) {//            synchronized (new Object()) {//没有效果!System.out.println(t.getName() + ":正在试衣服...");Thread.sleep(5000);}System.out.println(t.getName()+":结账离开");} catch (InterruptedException e) {e.printStackTrace();}}
}
在静态方法上使用synchronized

当在静态方法上使用synchronized后,该方法是一个同步方法.由于静态方法所属类,所以一定具有同步效果.

静态方法使用的同步监视器对象为当前类的类对象(Class的实例).

注:类对象会在后期反射知识点介绍.

package thread;/*** 静态方法上如果使用synchronized,则该方法一定具有同步效果。*/
public class SyncDemo3 {public static void main(String[] args) {Thread t1 = new Thread(){public void run(){Boo.dosome();}};Thread t2 = new Thread(){public void run(){Boo.dosome();}};t1.start();t2.start();}
}
class Boo{/*** synchronized在静态方法上使用是,指定的同步监视器对象为当前类的类对象。* 即:Class实例。* 在JVM中,每个被加载的类都有且只有一个Class的实例与之对应,后面讲反射* 知识点的时候会介绍类对象。*/public synchronized static void dosome(){Thread t = Thread.currentThread();try {System.out.println(t.getName() + ":正在执行dosome方法...");Thread.sleep(5000);System.out.println(t.getName() + ":执行dosome方法完毕!");} catch (InterruptedException e) {e.printStackTrace();}}}
}
静态方法中使用同步块时,指定的锁对象通常也是当前类的类对象
class Boo{public static void dosome(){/*静态方法中使用同步块时,指定同步监视器对象通常还是用当前类的类对象获取方式为:类名.class*/synchronized (Boo.class) {Thread t = Thread.currentThread();try {System.out.println(t.getName() + ":正在执行dosome方法...");Thread.sleep(5000);System.out.println(t.getName() + ":执行dosome方法完毕!");} catch (InterruptedException e) {e.printStackTrace();}}}
}
互斥锁

当多个线程执行不同的代码片段,但是这些代码片段之间不能同时运行时就要设置为互斥的.

使用synchronized锁定多个代码片段,并且指定的同步监视器是同一个时,这些代码片段之间就是互斥的.

package thread;/*** 互斥锁* 当使用synchronized锁定多个不同的代码片段,并且指定的同步监视器对象相同时,* 这些代码片段之间就是互斥的,即:多个线程不能同时访问这些方法。*/
public class SyncDemo4 {public static void main(String[] args) {Foo foo = new Foo();Thread t1 = new Thread(){public void run(){foo.methodA();}};Thread t2 = new Thread(){public void run(){foo.methodB();}};t1.start();t2.start();}
}
class Foo{public synchronized void methodA(){Thread t = Thread.currentThread();try {System.out.println(t.getName()+":正在执行A方法...");Thread.sleep(5000);System.out.println(t.getName()+":执行A方法完毕!");} catch (InterruptedException e) {e.printStackTrace();}}public synchronized void methodB(){Thread t = Thread.currentThread();try {System.out.println(t.getName()+":正在执行B方法...");Thread.sleep(5000);System.out.println(t.getName()+":执行B方法完毕!");} catch (InterruptedException e) {e.printStackTrace();}}
}

死锁

死锁的产生:

两个线程各自持有一个锁对象的同时等待对方先释放锁对象,此时会出现僵持状态。这个现象就是死锁。

package thread;/*** 死锁* 死锁的产生:* 两个线程各自持有一个锁对象的同时等待对方先释放锁对象,此时会出现僵持状态。* 这个现象就是死锁。*/
public class DeadLockDemo {//定义两个锁对象,"筷子"和"勺"public static Object chopsticks = new Object();public static Object spoon = new Object();public static void main(String[] args) {Thread np = new Thread(){public void run(){System.out.println("北方人开始吃饭.");System.out.println("北方人去拿筷子...");synchronized (chopsticks){System.out.println("北方人拿起了筷子开始吃饭...");try {Thread.sleep(5000);} catch (InterruptedException e) {}System.out.println("北方人吃完了饭,去拿勺...");synchronized (spoon){System.out.println("北方人拿起了勺子开始喝汤...");try {Thread.sleep(5000);} catch (InterruptedException e) {}System.out.println("北方人喝完了汤");}System.out.println("北方人放下了勺");}System.out.println("北方人放下了筷子,吃饭完毕!");}};Thread sp = new Thread(){public void run(){System.out.println("南方人开始吃饭.");System.out.println("南方人去拿勺...");synchronized (spoon){System.out.println("南方人拿起了勺开始喝汤...");try {Thread.sleep(5000);} catch (InterruptedException e) {}System.out.println("南方人喝完了汤,去拿筷子...");synchronized (chopsticks){System.out.println("南方人拿起了筷子开始吃饭...");try {Thread.sleep(5000);} catch (InterruptedException e) {}System.out.println("南方人吃完了饭");}System.out.println("南方人放下了筷子");}System.out.println("南方人放下了勺,吃饭完毕!");}};np.start();sp.start();}
}

聊天室(续)

实现服务端发送消息给客户端

在服务端通过Socket获取输出流,客户端获取输入流,实现服务端将消息发送给客户端.

这里让服务端直接将客户端发送过来的消息再回复给客户端来进行测试.

服务端代码:

package socket;import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;/*** 聊天室服务端*/
public class Server {/*** 运行在服务端的ServerSocket主要完成两个工作:* 1:向服务端操作系统申请服务端口,客户端就是通过这个端口与ServerSocket建立链接* 2:监听端口,一旦一个客户端建立链接,会立即返回一个Socket。通过这个Socket*   就可以和该客户端交互了** 我们可以把ServerSocket想象成某客服的"总机"。用户打电话到总机,总机分配一个* 电话使得服务端与你沟通。*/private ServerSocket serverSocket;/*** 服务端构造方法,用来初始化*/public Server(){try {System.out.println("正在启动服务端...");/*实例化ServerSocket时要指定服务端口,该端口不能与操作系统其他应用程序占用的端口相同,否则会抛出异常:java.net.BindException:address already in use端口是一个数字,取值范围:0-65535之间。6000之前的的端口不要使用,密集绑定系统应用和流行应用程序。*/serverSocket = new ServerSocket(8088);System.out.println("服务端启动完毕!");} catch (IOException e) {e.printStackTrace();}}/*** 服务端开始工作的方法*/public void start(){try {while(true) {System.out.println("等待客户端链接...");/*ServerSocket提供了接受客户端链接的方法:Socket accept()这个方法是一个阻塞方法,调用后方法"卡住",此时开始等待客户端的链接,直到一个客户端链接,此时该方法会立即返回一个Socket实例通过这个Socket就可以与客户端进行交互了。可以理解为此操作是接电话,电话没响时就一直等。*/Socket socket = serverSocket.accept();System.out.println("一个客户端链接了!");//启动一个线程与该客户端交互ClientHandler clientHandler = new ClientHandler(socket);Thread t = new Thread(clientHandler);t.start();}} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) {Server server = new Server();server.start();}/*** 定义线程任务* 目的是让一个线程完成与特定客户端的交互工作*/private class ClientHandler implements Runnable{private Socket socket;private String host;//记录客户端的IP地址信息public ClientHandler(Socket socket){this.socket = socket;//通过socket获取远端计算机地址信息host = socket.getInetAddress().getHostAddress();}public void run(){try{/*Socket提供的方法:InputStream getInputStream()获取的字节输入流读取的是对方计算机发送过来的字节*/InputStream in = socket.getInputStream();InputStreamReader isr = new InputStreamReader(in, "UTF-8");BufferedReader br = new BufferedReader(isr);OutputStream out = socket.getOutputStream();OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8");BufferedWriter bw = new BufferedWriter(osw);PrintWriter pw = new PrintWriter(bw,true);String message = null;while ((message = br.readLine()) != null) {System.out.println(host + "说:" + message);//将消息回复给客户端pw.println(host + "说:" + message);}}catch(IOException e){e.printStackTrace();}}}}

客户端代码:

package socket;import java.io.*;
import java.net.Socket;
import java.util.Scanner;/*** 聊天室客户端*/
public class Client {/*java.net.Socket 套接字Socket封装了TCP协议的通讯细节,我们通过它可以与远端计算机建立链接,并通过它获取两个流(一个输入,一个输出),然后对两个流的数据读写完成与远端计算机的数据交互工作。我们可以把Socket想象成是一个电话,电话有一个听筒(输入流),一个麦克风(输出流),通过它们就可以与对方交流了。*/private Socket socket;/*** 构造方法,用来初始化客户端*/public Client(){try {System.out.println("正在链接服务端...");/*实例化Socket时要传入两个参数参数1:服务端的地址信息可以是IP地址,如果链接本机可以写"localhost"参数2:服务端开启的服务端口我们通过IP找到网络上的服务端计算机,通过端口链接运行在该机器上的服务端应用程序。实例化的过程就是链接的过程,如果链接失败会抛出异常:java.net.ConnectException: Connection refused: connect*/socket = new Socket("localhost",8088);System.out.println("与服务端建立链接!");} catch (IOException e) {e.printStackTrace();}}/*** 客户端开始工作的方法*/public void start(){try {/*Socket提供了一个方法:OutputStream getOutputStream()该方法获取的字节输出流写出的字节会通过网络发送给对方计算机。*///低级流,将字节通过网络发送给对方OutputStream out = socket.getOutputStream();//高级流,负责衔接字节流与字符流,并将写出的字符按指定字符集转字节OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8");//高级流,负责块写文本数据加速BufferedWriter bw = new BufferedWriter(osw);//高级流,负责按行写出字符串,自动行刷新PrintWriter pw = new PrintWriter(bw,true);//通过socket获取输入流读取服务端发送过来的消息InputStream in = socket.getInputStream();InputStreamReader isr = new InputStreamReader(in,"UTF-8");BufferedReader br = new BufferedReader(isr);Scanner scanner = new Scanner(System.in);while(true) {String line = scanner.nextLine();if("exit".equalsIgnoreCase(line)){break;}pw.println(line);line = br.readLine();System.out.println(line);}} catch (IOException e) {e.printStackTrace();} finally {try {/*通讯完毕后调用socket的close方法。该方法会给对方发送断开信号。*/socket.close();} catch (IOException e) {e.printStackTrace();}}}public static void main(String[] args) {Client client = new Client();client.start();}
}

服务端转发消息给所有客户端

当一个客户端发送一个消息后,服务端收到后如何转发给所有客户端.

问题:例如红色的线程一收到客户端消息后如何获取到橙色的线程二中的输出流?得不到就无法将消息转发给橙色的客户端(进一步延伸就是无法转发给所有其他客户端)

解决:内部类可以访问外部类的成员,因此在Server类上定义一个数组allOut可以被所有内部类ClientHandler实例访问.从而将这些ClientHandler实例之间想互访的数据存放在这个数组中达到共享数据的目的.对此只需要将所有ClientHandler中的输出流都存入到数组allOut中就可以达到互访输出流转发消息的目的了.

服务端代码:

package socket;import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Arrays;/*** 聊天室服务端*/
public class Server {/*** 运行在服务端的ServerSocket主要完成两个工作:* 1:向服务端操作系统申请服务端口,客户端就是通过这个端口与ServerSocket建立链接* 2:监听端口,一旦一个客户端建立链接,会立即返回一个Socket。通过这个Socket*   就可以和该客户端交互了** 我们可以把ServerSocket想象成某客服的"总机"。用户打电话到总机,总机分配一个* 电话使得服务端与你沟通。*/private ServerSocket serverSocket;/*存放所有客户端输出流,用于广播消息*/private PrintWriter[] allOut = {};/*** 服务端构造方法,用来初始化*/public Server(){try {System.out.println("正在启动服务端...");/*实例化ServerSocket时要指定服务端口,该端口不能与操作系统其他应用程序占用的端口相同,否则会抛出异常:java.net.BindException:address already in use端口是一个数字,取值范围:0-65535之间。6000之前的的端口不要使用,密集绑定系统应用和流行应用程序。*/serverSocket = new ServerSocket(8088);System.out.println("服务端启动完毕!");} catch (IOException e) {e.printStackTrace();}}/*** 服务端开始工作的方法*/public void start(){try {while(true) {System.out.println("等待客户端链接...");/*ServerSocket提供了接受客户端链接的方法:Socket accept()这个方法是一个阻塞方法,调用后方法"卡住",此时开始等待客户端的链接,直到一个客户端链接,此时该方法会立即返回一个Socket实例通过这个Socket就可以与客户端进行交互了。可以理解为此操作是接电话,电话没响时就一直等。*/Socket socket = serverSocket.accept();System.out.println("一个客户端链接了!");//启动一个线程与该客户端交互ClientHandler clientHandler = new ClientHandler(socket);Thread t = new Thread(clientHandler);t.start();}} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) {Server server = new Server();server.start();}/*** 定义线程任务* 目的是让一个线程完成与特定客户端的交互工作*/private class ClientHandler implements Runnable{private Socket socket;private String host;//记录客户端的IP地址信息public ClientHandler(Socket socket){this.socket = socket;//通过socket获取远端计算机地址信息host = socket.getInetAddress().getHostAddress();}public void run(){try{/*Socket提供的方法:InputStream getInputStream()获取的字节输入流读取的是对方计算机发送过来的字节*/InputStream in = socket.getInputStream();InputStreamReader isr = new InputStreamReader(in, "UTF-8");BufferedReader br = new BufferedReader(isr);OutputStream out = socket.getOutputStream();OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8");BufferedWriter bw = new BufferedWriter(osw);PrintWriter pw = new PrintWriter(bw,true);//将该输出流存入共享数组allOut中//1对allOut数组扩容allOut = Arrays.copyOf(allOut,allOut.length+1);//2将输出流存入数组最后一个位置allOut[allOut.length-1] = pw;String message = null;while ((message = br.readLine()) != null) {System.out.println(host + "说:" + message);//将消息回复给所有客户端for(int i=0;i<allOut.length;i++) {allOut[i].println(host + "说:" + message);}}}catch(IOException e){e.printStackTrace();}}}}

客户端解决收发消息的冲突问题

由于客户端start方法中循环进行的操作顺序是先通过控制台输入一句话后将其发送给服务端,然后再读取服务端发送回来的一句话.这导致如果客户端不输入内容就无法收到服务端发送过来的其他信息(其他客户端的聊天内容).因此要将客户端中接收消息的工作移动到一个单独的线程上执行,才能保证收发消息互不打扰.

客户端代码:

package socket;import java.io.*;
import java.net.Socket;
import java.util.Scanner;/*** 聊天室客户端*/
public class Client {/*java.net.Socket 套接字Socket封装了TCP协议的通讯细节,我们通过它可以与远端计算机建立链接,并通过它获取两个流(一个输入,一个输出),然后对两个流的数据读写完成与远端计算机的数据交互工作。我们可以把Socket想象成是一个电话,电话有一个听筒(输入流),一个麦克风(输出流),通过它们就可以与对方交流了。*/private Socket socket;/*** 构造方法,用来初始化客户端*/public Client(){try {System.out.println("正在链接服务端...");/*实例化Socket时要传入两个参数参数1:服务端的地址信息可以是IP地址,如果链接本机可以写"localhost"参数2:服务端开启的服务端口我们通过IP找到网络上的服务端计算机,通过端口链接运行在该机器上的服务端应用程序。实例化的过程就是链接的过程,如果链接失败会抛出异常:java.net.ConnectException: Connection refused: connect*/socket = new Socket("localhost",8088);System.out.println("与服务端建立链接!");} catch (IOException e) {e.printStackTrace();}}/*** 客户端开始工作的方法*/public void start(){try {//启动读取服务端发送过来消息的线程ServerHandler handler = new ServerHandler();Thread t = new Thread(handler);t.setDaemon(true);t.start();/*Socket提供了一个方法:OutputStream getOutputStream()该方法获取的字节输出流写出的字节会通过网络发送给对方计算机。*///低级流,将字节通过网络发送给对方OutputStream out = socket.getOutputStream();//高级流,负责衔接字节流与字符流,并将写出的字符按指定字符集转字节OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8");//高级流,负责块写文本数据加速BufferedWriter bw = new BufferedWriter(osw);//高级流,负责按行写出字符串,自动行刷新PrintWriter pw = new PrintWriter(bw,true);Scanner scanner = new Scanner(System.in);while(true) {String line = scanner.nextLine();if("exit".equalsIgnoreCase(line)){break;}pw.println(line);}} catch (IOException e) {e.printStackTrace();} finally {try {/*通讯完毕后调用socket的close方法。该方法会给对方发送断开信号。*/socket.close();} catch (IOException e) {e.printStackTrace();}}}public static void main(String[] args) {Client client = new Client();client.start();}/*** 该线程负责接收服务端发送过来的消息*/private class ServerHandler implements Runnable{public void run(){//通过socket获取输入流读取服务端发送过来的消息try {InputStream in = socket.getInputStream();InputStreamReader isr = new InputStreamReader(in,"UTF-8");BufferedReader br = new BufferedReader(isr);String line;//循环读取服务端发送过来的每一行字符串while((line = br.readLine())!=null){System.out.println(line);}} catch (IOException e) {e.printStackTrace();}}}
}

服务端完成处理客户端断开连接后的操作

当一个客户端断开连接后,服务端处理该客户端交互的线程ClientHandler应当将通过socket获取的输出流从共享数组allOut中删除,防止其他的ClientHandler再将消息通过这个输出流发送给当前客户端.

服务端代码:

package socket;import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Arrays;/*** 聊天室服务端*/
public class Server {/*** 运行在服务端的ServerSocket主要完成两个工作:* 1:向服务端操作系统申请服务端口,客户端就是通过这个端口与ServerSocket建立链接* 2:监听端口,一旦一个客户端建立链接,会立即返回一个Socket。通过这个Socket*   就可以和该客户端交互了** 我们可以把ServerSocket想象成某客服的"总机"。用户打电话到总机,总机分配一个* 电话使得服务端与你沟通。*/private ServerSocket serverSocket;/*存放所有客户端输出流,用于广播消息*/private PrintWriter[] allOut = {};/*** 服务端构造方法,用来初始化*/public Server(){try {System.out.println("正在启动服务端...");/*实例化ServerSocket时要指定服务端口,该端口不能与操作系统其他应用程序占用的端口相同,否则会抛出异常:java.net.BindException:address already in use端口是一个数字,取值范围:0-65535之间。6000之前的的端口不要使用,密集绑定系统应用和流行应用程序。*/serverSocket = new ServerSocket(8088);System.out.println("服务端启动完毕!");} catch (IOException e) {e.printStackTrace();}}/*** 服务端开始工作的方法*/public void start(){try {while(true) {System.out.println("等待客户端链接...");/*ServerSocket提供了接受客户端链接的方法:Socket accept()这个方法是一个阻塞方法,调用后方法"卡住",此时开始等待客户端的链接,直到一个客户端链接,此时该方法会立即返回一个Socket实例通过这个Socket就可以与客户端进行交互了。可以理解为此操作是接电话,电话没响时就一直等。*/Socket socket = serverSocket.accept();System.out.println("一个客户端链接了!");//启动一个线程与该客户端交互ClientHandler clientHandler = new ClientHandler(socket);Thread t = new Thread(clientHandler);t.start();}} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) {Server server = new Server();server.start();}/*** 定义线程任务* 目的是让一个线程完成与特定客户端的交互工作*/private class ClientHandler implements Runnable{private Socket socket;private String host;//记录客户端的IP地址信息public ClientHandler(Socket socket){this.socket = socket;//通过socket获取远端计算机地址信息host = socket.getInetAddress().getHostAddress();}public void run(){PrintWriter pw = null;try{/*Socket提供的方法:InputStream getInputStream()获取的字节输入流读取的是对方计算机发送过来的字节*/InputStream in = socket.getInputStream();InputStreamReader isr = new InputStreamReader(in, "UTF-8");BufferedReader br = new BufferedReader(isr);OutputStream out = socket.getOutputStream();OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8");BufferedWriter bw = new BufferedWriter(osw);pw = new PrintWriter(bw,true);//将该输出流存入共享数组allOut中//1对allOut数组扩容allOut = Arrays.copyOf(allOut, allOut.length + 1);//2将输出流存入数组最后一个位置allOut[allOut.length - 1] = pw;//通知所有客户端该用户上线了sendMessage(host + "上线了,当前在线人数:"+allOut.length);String message = null;while ((message = br.readLine()) != null) {System.out.println(host + "说:" + message);//将消息回复给所有客户端sendMessage(host + "说:" + message);}}catch(IOException e){e.printStackTrace();}finally{//处理客户端断开链接的操作//将当前客户端的输出流从allOut中删除(数组缩容)for(int i=0;i<allOut.length;i++){if(allOut[i]==pw){allOut[i] = allOut[allOut.length-1];allOut = Arrays.copyOf(allOut,allOut.length-1);break;}}sendMessage(host+"下线了,当前在线人数:"+allOut.length);try {socket.close();//与客户端断开链接} catch (IOException e) {e.printStackTrace();}}}/*** 广播消息给所有客户端* @param message*/private void sendMessage(String message){for(int i=0;i<allOut.length;i++) {allOut[i].println(message);}}}}

服务端解决多线程并发安全问题

为了让能叫消息转发给所有客户端,我们 在Server上添加了一个数组类型的属性allOut,并且共所有线程ClientHandler使用,这时对数组的操作要考虑并发安全问题

当两个客户端同时上线(橙,绿)

两个ClientHandler启动后都会对数组扩容,将自身的输出流存入数组

此时ClientHandler(橙)先拿到CPU时间,进行数组扩容

扩容后发生CPU切换,ClientHandler(绿)拿到时间

此时ClientHandler(绿)进行数组扩容

ClientHandler(绿)扩容后,将输出流存入数组最后一个位置

线程切换回ClientHandler(橙)

ClientHandler(橙)将输出流存入数组最后一个位置,此时会将ClientHandler(绿)存入的输入流覆盖。出现了并发安全问题!!

选取合适的锁对象

this不可以

allOut不可以。大多数情况下可以选择临界资源作为锁对象,但是这里不行。

ClientHandler(橙)锁定allOut

ClientHandler(橙)扩容allOut

由于数组是定长的,扩容实际是创建新数组,因此扩容后赋值给allOut时,ClientHandler(橙)之前锁定的对象就被GC回收了!而新扩容的数组并没有锁。

若此时发生线程切换,ClientHandler(绿)锁定allOut时,发现该allOut没有锁,因此可以锁定,并执行synchronized内部代码

ClientHandler(绿)也可以进行数组扩容,那么它之前锁定的数组也被GC回收了!

从上述代码可以看出,锁定allOut并没有限制多个线程(ClientHandler)操作allOut数组,还是存在并发安全问题。

可以选取外部类对象作为锁对象,因为这些内部类ClientHandler都从属于这个外部类对象Server.this

还要考虑对数组的不同操作之间的互斥问题,道理同上。因此,对allOut数组的扩容,缩容和遍历操作要进行互斥。

最终代码:

package socket;import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Arrays;/*** 聊天室服务端*/
public class Server {/*** 运行在服务端的ServerSocket主要完成两个工作:* 1:向服务端操作系统申请服务端口,客户端就是通过这个端口与ServerSocket建立链接* 2:监听端口,一旦一个客户端建立链接,会立即返回一个Socket。通过这个Socket*   就可以和该客户端交互了** 我们可以把ServerSocket想象成某客服的"总机"。用户打电话到总机,总机分配一个* 电话使得服务端与你沟通。*/private ServerSocket serverSocket;/*存放所有客户端输出流,用于广播消息*/private PrintWriter[] allOut = {};/*** 服务端构造方法,用来初始化*/public Server(){try {System.out.println("正在启动服务端...");/*实例化ServerSocket时要指定服务端口,该端口不能与操作系统其他应用程序占用的端口相同,否则会抛出异常:java.net.BindException:address already in use端口是一个数字,取值范围:0-65535之间。6000之前的的端口不要使用,密集绑定系统应用和流行应用程序。*/serverSocket = new ServerSocket(8088);System.out.println("服务端启动完毕!");} catch (IOException e) {e.printStackTrace();}}/*** 服务端开始工作的方法*/public void start(){try {while(true) {System.out.println("等待客户端链接...");/*ServerSocket提供了接受客户端链接的方法:Socket accept()这个方法是一个阻塞方法,调用后方法"卡住",此时开始等待客户端的链接,直到一个客户端链接,此时该方法会立即返回一个Socket实例通过这个Socket就可以与客户端进行交互了。可以理解为此操作是接电话,电话没响时就一直等。*/Socket socket = serverSocket.accept();System.out.println("一个客户端链接了!");//启动一个线程与该客户端交互ClientHandler clientHandler = new ClientHandler(socket);Thread t = new Thread(clientHandler);t.start();}} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) {Server server = new Server();server.start();}/*** 定义线程任务* 目的是让一个线程完成与特定客户端的交互工作*/private class ClientHandler implements Runnable{private Socket socket;private String host;//记录客户端的IP地址信息public ClientHandler(Socket socket){this.socket = socket;//通过socket获取远端计算机地址信息host = socket.getInetAddress().getHostAddress();}public void run(){PrintWriter pw = null;try{/*Socket提供的方法:InputStream getInputStream()获取的字节输入流读取的是对方计算机发送过来的字节*/InputStream in = socket.getInputStream();InputStreamReader isr = new InputStreamReader(in, "UTF-8");BufferedReader br = new BufferedReader(isr);OutputStream out = socket.getOutputStream();OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8");BufferedWriter bw = new BufferedWriter(osw);pw = new PrintWriter(bw,true);//将该输出流存入共享数组allOut中
//                synchronized (this) {//不行,因为这个是ClientHandler实例
//                synchronized (allOut) {//不行,下面操作会扩容,allOut对象会变synchronized (Server.this) {//外部类对象可以//1对allOut数组扩容allOut = Arrays.copyOf(allOut, allOut.length + 1);//2将输出流存入数组最后一个位置allOut[allOut.length - 1] = pw;}//通知所有客户端该用户上线了sendMessage(host + "上线了,当前在线人数:"+allOut.length);String message = null;while ((message = br.readLine()) != null) {System.out.println(host + "说:" + message);//将消息回复给所有客户端sendMessage(host + "说:" + message);}}catch(IOException e){e.printStackTrace();}finally{//处理客户端断开链接的操作//将当前客户端的输出流从allOut中删除(数组缩容)synchronized (Server.this) {for (int i = 0; i < allOut.length; i++) {if (allOut[i] == pw) {allOut[i] = allOut[allOut.length - 1];allOut = Arrays.copyOf(allOut, allOut.length - 1);break;}}}sendMessage(host+"下线了,当前在线人数:"+allOut.length);try {socket.close();//与客户端断开链接} catch (IOException e) {e.printStackTrace();}}}/*** 广播消息给所有客户端* @param message*/private void sendMessage(String message){synchronized (Server.this) {for (int i = 0; i < allOut.length; i++) {allOut[i].println(message);}}}}
}

集合框架

什么是集合

集合与数组一样,可以保存一组元素,并且提供了操作元素的相关方法,使用更方便.

java集合框架中相关接口

java.util.Collection接口:

java.util.Collection是所有集合的顶级接口.Collection下面有多种实现类,因此我们有更多的数据结构可供选择.

Collection下面有两个常见的子接口:

  • java.util.List:线性表.是可重复集合,并且有序.
  • java.util.Set:不可重复的集合,大部分实现类是无序的.

这里可重复指的是集合中的元素是否可以重复,而判定重复元素的标准是依靠元素自身equals比较

的结果.为true就认为是重复元素.

package collection;import java.util.ArrayList;
import java.util.Collection;public class CollectionDemo {public static void main(String[] args) {Collection c = new ArrayList();/*boolean add(E e)向当前集合中添加一个元素.当元素成功添加后返回true*/c.add("one");c.add("two");c.add("three");c.add("four");c.add("five");System.out.println(c);/*int size()返回当前集合的元素个数*/int size = c.size();System.out.println("size:"+size);/*boolean isEmpty()判断当前集合是否为空集(不含有任何元素)*/boolean isEmpty = c.isEmpty();System.out.println("是否为空集:"+isEmpty);/*清空集合*/c.clear();System.out.println(c);System.out.println("size:"+c.size());//0System.out.println("是否为空集:"+c.isEmpty());}
}

集合与元素equals方法相关的方法

package collection;import java.util.Objects;/*** 使用当前类作为集合元素测试集合相关操作*/
public class Point {private int x;private int y;public Point(int x, int y) {this.x = x;this.y = y;}public int getX() {return x;}public void setX(int x) {this.x = x;}public int getY() {return y;}public void setY(int y) {this.y = y;}@Overridepublic String toString() {return "Point{" +"x=" + x +", y=" + y +'}';}@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;Point point = (Point) o;return x == point.x && y == point.y;}@Overridepublic int hashCode() {return Objects.hash(x, y);}
}
package collection;import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;/*** 集合的很多操作有与元素的equals方法相关。*/
public class CollectionDemo2 {public static void main(String[] args) {//        Collection c = new ArrayList();Collection c = new HashSet();c.add(new Point(1,2));c.add(new Point(3,4));c.add(new Point(5,6));c.add(new Point(7,8));c.add(new Point(1,2));/*集合重写了Object的toString方法,输出的格式为:[元素1.toString(), 元素2.toString(), ....]*/System.out.println(c);Point p = new Point(1,2);/*boolean contains(Object o)判断当前集合是否包含给定元素,这里判断的依据是给定元素是否与集合现有元素存在equals比较为true的情况。*/boolean contains = c.contains(p);System.out.println("包含:"+contains);/*remove用来从集合中删除给定元素,删除的也是与集合中equals比较为true的元素。注意,对于可以存放重复元素的集合而言,只删除一次。*/c.remove(p);System.out.println(c);}
}

集合存放的是元素的引用

集合只能存放引用类型元素,并且存放的是元素的引用

package collection;import java.util.ArrayList;
import java.util.Collection;/*** 集合只能存放引用类型元素,并且存放的是元素的引用(地址)*/
public class CollectionDemo3 {public static void main(String[] args) {Collection c = new ArrayList();Point p = new Point(1,2);c.add(p);System.out.println("p:"+p);//p:(1,2)System.out.println("c:"+c);//c:[(1,2)]p.setX(2);System.out.println("p:"+p);//p:(2,2)System.out.println("c:"+c);//c:[(2,2)]}
}


集合间的操作

集合提供了如取并集,删交集,判断包含子集等操作

package collection;import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;/*** 集合间的操作*/
public class CollectionDemo4 {public static void main(String[] args) {//        Collection c1 = new ArrayList();Collection c1 = new HashSet();//不可重复元素c1.add("java");c1.add("c");c1.add("c++");System.out.println("c1:"+c1);Collection c2 = new ArrayList();c2.add("android");c2.add("ios");c2.add("java");System.out.println("c2:"+c2);/*boolean addAll(Collection c)将给定集合中的所有元素添加到当前集合中。当前集合若发生了改变则返回true*/boolean tf = c1.addAll(c2);System.out.println(tf);System.out.println("c1:"+c1);System.out.println("c2:"+c2);Collection c3 = new ArrayList();c3.add("ios");c3.add("c++");c3.add("php");System.out.println("c3:"+c3);/*boolean containsAll(Collection c)判断当前集合是否包含给定集合中的所有元素*/boolean contains = c1.containsAll(c3);System.out.println("包含所有元素:"+contains);/*boolean removeAll(Collection c)删除当前集合中与给定集合中的共有元素*/c1.removeAll(c3);System.out.println("c1:"+c1);System.out.println("c3:"+c3);}
}

集合的遍历

Collection提供了统一的遍历集合方式:迭代器模式

Iterator iterator()

该方法会获取一个用于遍历当前集合元素的迭代器.

java.util.Iterator接口

迭代器接口,定义了迭代器遍历集合的相关操作.

不同的集合都实现了一个用于遍历自身元素的迭代器实现类,我们无需记住它们的名字,用多态的角度把他们看做为Iterator即可.

迭代器遍历集合遵循的步骤为:问,取,删.其中删除元素不是必要操作

package collection;import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;/*** Collection接口没有定义单独获取某一个元素的操作,因为不通用。* 但是Collection提供了遍历集合元素的操作。该操作是一个通用操作,无论什么类型的* 集合都支持此种遍历方式:迭代器模式。** Iterator iterator()           die(二声)* 该方法会获取一个用于遍历当前集合元素的迭代器** java.util.Iterator接口,是迭代器接口,规定了迭代器遍历集合的相关操作,不同的* 集合都提供了一个用于遍历自身元素的迭代器实现类,不过我们不需要直到它们的名字,以* 多态的方式当成Iterator使用即可。* 迭代器遍历集合遵循的步骤为:问->取->删* 其中删除不是必须操作。**/
public class IteratorDemo {public static void main(String[] args) {Collection c = new ArrayList();c.add("one");c.add("two");c.add("three");c.add("four");c.add("five");System.out.println(c);//获取迭代器Iterator it = c.iterator();/*迭代器提供的相关方法:boolean hasNext()判断集合是否还有元素可以遍历E next()获取集合下一个元素(第一次调用时就是获取第一个元素,以此类推)*/while(it.hasNext()){String str = (String)it.next();System.out.println(str);         }System.out.println(c);}
}

迭代器遍历过程中不得通过集合的方法增删元素

package collection;import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;/*** Collection接口没有定义单独获取某一个元素的操作,因为不通用。* 但是Collection提供了遍历集合元素的操作。该操作是一个通用操作,无论什么类型的* 集合都支持此种遍历方式:迭代器模式。** Iterator iterator()           die(二声)* 该方法会获取一个用于遍历当前集合元素的迭代器** java.util.Iterator接口,是迭代器接口,规定了迭代器遍历集合的相关操作,不同的* 集合都提供了一个用于遍历自身元素的迭代器实现类,不过我们不需要直到它们的名字,以* 多态的方式当成Iterator使用即可。* 迭代器遍历集合遵循的步骤为:问->取->删* 其中删除不是必须操作。**/
public class IteratorDemo {public static void main(String[] args) {Collection c = new ArrayList();c.add("one");c.add("#");c.add("two");c.add("#");c.add("three");c.add("#");c.add("four");c.add("#");c.add("five");System.out.println(c);//获取迭代器Iterator it = c.iterator();/*迭代器提供的相关方法:boolean hasNext()判断集合是否还有元素可以遍历E next()获取集合下一个元素(第一次调用时就是获取第一个元素,以此类推)*/while(it.hasNext()){String str = (String)it.next();System.out.println(str);if("#".equals(str)){/*迭代器要求遍历的过程中不得通过集合的方法增删元素否则会抛出异常:ConcurrentModificationException*/
//                c.remove(str);/*迭代器的remove方法可以将通过next方法获取的元素从集合中删除。*/it.remove();}}System.out.println(c);}
}

增强型for循环

JDK5之后推出了一个特性:增强型for循环

  • 也称为新循环,使得我们可以使用相同的语法遍历集合或数组.
  • 语法:
for(元素类型 变量名 : 集合或数组){循环体
}
package collection;import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;/*** JDK5推出时,推出了一个新的特性:增强型for循环* 也称为新循环,它可以用相同的语法遍历集合或数组。** 新循环是java编译器认可的,并非虚拟机。*/
public class NewForDemo {public static void main(String[] args) {String[] array = {"one","two","three","four","five"};for(int i=0;i<array.length;i++){String str = array[i];System.out.println(str);}for(String str : array){System.out.println(str);}Collection c = new ArrayList();c.add("one");c.add("two");c.add("three");c.add("four");c.add("five");//迭代器遍历Iterator it = c.iterator();while(it.hasNext()){String str = (String)it.next();System.out.println(str);}//新循环遍历for(Object o : c){String str = (String)o;System.out.println(str);}}
}

泛型

JDK5之后推出的另一个特性:泛型

泛型也称为参数化类型,允许我们在使用一个类时指定它当中属性,方法参数或返回值的类型.

  • 泛型在集合中被广泛使用,用来指定集合中的元素类型.
  • 有泛型支持的类在使用时若不指定泛型的具体类型则默认为原型Object
package collection;import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;/*** JDK5推出时,推出了一个新的特性:增强型for循环* 也称为新循环,它可以用相同的语法遍历集合或数组。** 新循环是java编译器认可的,并非虚拟机。*/
public class NewForDemo {public static void main(String[] args) {String[] array = {"one","two","three","four","five"};for(int i=0;i<array.length;i++){String str = array[i];System.out.println(str);}for(String str : array){System.out.println(str);}/**  泛型 JDK5之后推出的另一个特性。* 泛型也称为参数化类型,允许我们在使用一个类时指定它里面属性的类型,* 方法参数或返回值的类型,使得我们使用一个类时可以更灵活。* 泛型被广泛应用于集合中,用来指定集合中的元素类型。* 支持泛型的类在使用时如果未指定泛型,那么默认就是原型Object** Collection接口的定义* public interface Collection<E> ... {** Collection<E> 这里的<E>就是泛型** Collection中add方法的定义,参数为E* boolean add(E e)*/Collection<String> c = new ArrayList<>();c.add("one");//编译器会检查add方法的实参是否为String类型c.add("two");c.add("three");c.add("four");c.add("five");
//        c.add(123);//编译不通过//迭代器遍历//迭代器也支持泛型,指定的与其遍历的集合指定的泛型一致即可Iterator<String> it = c.iterator();while(it.hasNext()){//编译器编译代码时会根据迭代器指定的泛型补充造型代码String str = it.next();//获取元素时无需在造型System.out.println(str);}//新循环遍历for(String str : c){System.out.println(str);}}
}

List集

java.util.List接口,继承自Collection.

List集合是可重复集,并且有序,提供了一套可以通过下标操作元素的方法

常用实现类:

  • java.util.ArrayList:内部使用数组实现,查询性能更好.
  • java.util.LinkedList:内部使用链表实现,首尾增删元素性能更好.

List集合常见方法

get()与set()

package collection;import java.util.ArrayList;
import java.util.List;/***  List集合*  List是Collection下面常见的一类集合。*  java.util.List接口是所有List的接口,它继承自Collection。*  常见的实现类:*  java.util.ArrayList:内部由数组实现,查询性能更好。*  java.util.LinkedList:内部由链表实现,增删性能更好。**  List集合的特点是:可以存放重复元素,并且有序。其提供了一套可以通过下标*  操作元素的方法。*/
public class ListDemo {public static void main(String[] args) {List<String> list = new ArrayList<>();
//        List<String> list = new LinkedList<>();list.add("one");list.add("two");list.add("three");list.add("four");list.add("five");/*E get(int index)获取指定下标对应的元素*///获取第三个元素String e = list.get(2);System.out.println(e);for(int i=0;i<list.size();i++){e = list.get(i);System.out.println(e);}/*E set(int index,E e)将给定元素设置到指定位置,返回值为该位置原有的元素。替换元素操作*///[one,six,three,four,five]String old = list.set(1,"six");System.out.println(list);System.out.println("被替换的元素是:"+old);}
}

重载的add()和remove()

package collection;import java.util.ArrayList;
import java.util.List;/*** List集合提供了一对重载的add,remove方法*/
public class ListDemo2 {public static void main(String[] args) {List<String> list = new ArrayList<>();list.add("one");list.add("two");list.add("three");list.add("four");list.add("five");System.out.println(list);/*void add(int index,E e)将给定元素插入到指定位置*///[one,two,six,three,four,five]list.add(2,"six");System.out.println(list);/*E remove(int index)删除并返回指定位置上的元素*///[one,six,three,four,five]String e = list.remove(1);System.out.println(list);System.out.println("被删除的元素:"+e);}
}

subList()方法

package collection;import java.util.ArrayList;
import java.util.List;/***  List subList(int start,int end)*  获取当前集合中指定范围内的子集。两个参数为开始与结束的下标(含头不含尾)*/
public class ListDemo3 {public static void main(String[] args) {List<Integer> list = new ArrayList<>();for(int i=0;i<10;i++){list.add(i);}System.out.println(list);//获取3-7这部分List<Integer> subList = list.subList(3,8);System.out.println(subList);//将子集每个元素扩大10倍for(int i=0;i<subList.size();i++){subList.set(i,subList.get(i) * 10);}//[30,40,50,60,70]System.out.println(subList);/*对子集元素的操作就是对原集合对应元素的操作*/System.out.println(list);//删除list集合中的2-8list.subList(2,9).clear();System.out.println(list);}
}

集合与数组的转换

集合转换为数组

Collection提供了一个方法:toArray,可以将当前集合转换为一个数组

package collection;import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;/*** 集合转换为数组* Collection提供了方法toArray可以将当前集合转换为一个数组*/
public class CollectionToArrayDemo {public static void main(String[] args) {List<String> list = new ArrayList<>();list.add("one");list.add("two");list.add("three");list.add("four");list.add("five");System.out.println(list);//        Object[] array = list.toArray();/*重载的toArray方法要求传入一个数组,内部会将集合所有元素存入该数组后将其返回(前提是该数组长度>=集合的size)。如果给定的数组长度不足,则方法内部会自行根据给定数组类型创建一个与集合size一致长度的数组并将集合元素存入后返回。*/String[] array = list.toArray(new String[list.size()]);System.out.println(array.length);System.out.println(Arrays.toString(array));}
}

变长参数

JDK5时推出的另一个特性:变长参数

一个方法中只能声明一个变长参数,并且必须是最后一个参数

package collection;import java.util.Arrays;public class ArgDemo {public static void main(String[] args) {dosome(1,"a");dosome(1,"a","b");dosome(1,"a","b","a","b","a","b","a","b","a","b");dosome(1,new String[]{"1","2","3"});}public static void dosome(int i,String... s){/*变长参数在方法中实际上就是一个数组.给变长参数传入了几个实参,该数组长度与实参个数一致.*/System.out.println(s.length);System.out.println("s:"+ Arrays.toString(s));}}

数组转换为List集合

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

package collection;import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;/*** 数组转换为List集合* 数组的工具类Arrays提供了一个静态方法asList,可以将数组转换为一个List集合。*/
public class ArrayToListDemo {public static void main(String[] args) {String[] array = {"one","two","three","four","five"};System.out.println(Arrays.toString(array));List<String> list = Arrays.asList(array);System.out.println(list);list.set(1,"six");System.out.println(list);//数组跟着改变了。注意:对数组转换的集合进行元素操作就是对原数组对应的操作System.out.println(Arrays.toString(array));/*由于数组是定长的,因此对该集合进行增删元素的操作是不支持的,会抛出异常:java.lang.UnsupportedOperationException*/
//        list.add("seven");/*若希望对集合进行增删操作,则需要自行创建一个集合,然后将该集合元素导入。*/
//        List<String> list2 = new ArrayList<>();
//        list2.addAll(list);/*所有的集合都支持一个参数为Collection的构造方法,作用是在创建当前集合的同时包含给定集合中的所有元素*/List<String> list2 = new ArrayList<>(list);System.out.println("list2:"+list2);list2.add("seven");System.out.println("list2:"+list2);}
}

集合的排序

java.util.Collections类

Collections是集合的工具类,里面定义了很多静态方法用于操作集合.

Collections.sort(List list)方法

可以对List集合进行自然排序(从小到大)

package collection;import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;/*** 集合的排序* 集合的工具类:java.util.Collections提供了一个静态方法sort,可以对List集合* 进行自然排序*/
public class SortListDemo1 {public static void main(String[] args) {List<Integer> list = new ArrayList<>();Random random = new Random();for(int i=0;i<10;i++){list.add(random.nextInt(100));}System.out.println(list);Collections.sort(list);System.out.println(list);}
}

排序自定义类型元素

package collection;import java.util.ArrayList;
import java.util.Collections;
import java.util.List;/*** 排序自定义类型元素*/
public class SortListDemo2 {public static void main(String[] args) {List<Point> list = new ArrayList<>();list.add(new Point(1,2));list.add(new Point(97,88));list.add(new Point(7,6));list.add(new Point(9,9));list.add(new Point(5,4));list.add(new Point(2,3));System.out.println(list);/*编译不通过的原因:Collections.sort(List list)该方法要求集合中的元素类型必须实现接口:Comparable,该接口中有一个抽象方法compareTo,这个方法用来定义元素之间比较大小的规则.所以只有实现了该接口的元素才能利用这个方法比较出大小进而实现排序操作.*/Collections.sort(list);//编译不通过 compare比较  comparable可以比较的System.out.println(list);}
}

实际开发中,我们并不会让我们自己定义的类(如果该类作为集合元素使用)去实现Comparable接口,因为这对我们的程序有侵入性.

侵入性:当我们调用某个API功能时,其要求我们为其修改其他额外的代码,这个现象就是侵入性.侵入性越强的API越不利于程序的后期可维护性.应当尽量避免.

重载的Collections.sort(List list,Comparator c)方法

package collection;import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;/*** 排序自定义类型元素*/
public class SortListDemo2 {public static void main(String[] args) {List<Point> list = new ArrayList<>();list.add(new Point(1,2));list.add(new Point(97,88));list.add(new Point(7,6));list.add(new Point(9,9));list.add(new Point(5,4));list.add(new Point(2,3));System.out.println(list);/*Collections.sort(List list)在排序List集合时要求集合元素必须实现了Comparable接口。实现了该接口的类必须重写一个方法compareTo用与定义比较大小的规则,从而进行元素间的比较后排序。否则编译不通过。侵入性:当我们调用某个API时,其反过来要求我们为其修改其他额外的代码,这种现象就成为侵入性。侵入性不利于程序后期的维护,尽可能避免。compare:比较*/
//        Collections.sort(list);//匿名内部类的形式创建一个比较器Comparator<Point> com = new Comparator<Point>() {@Override/*** 实现比较器接口后必须重写方法compare.* 该方法用来定义参数o1与参数o2的比较大小规则* 返回值用来表示o1与o2的大小关系*/public int compare(Point o1, Point o2) {int len1 = o1.getX() * o1.getX() + o1.getY() * o1.getY();int len2 = o2.getX() * o2.getX() + o2.getY() * o2.getY();return len1-len2;}};Collections.sort(list,com);//回调模式System.out.println(list);}
}
最终没有侵入性的写法
package collection;import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;/*** 排序自定义类型元素*/
public class SortListDemo2 {public static void main(String[] args) {List<Point> list = new ArrayList<>();list.add(new Point(1,2));list.add(new Point(97,88));list.add(new Point(7,6));list.add(new Point(9,9));list.add(new Point(5,4));list.add(new Point(2,3));System.out.println(list);/*Collections.sort(List list)在排序List集合时要求集合元素必须实现了Comparable接口。实现了该接口的类必须重写一个方法compareTo用与定义比较大小的规则,从而进行元素间的比较后排序。否则编译不通过。侵入性:当我们调用某个API时,其反过来要求我们为其修改其他额外的代码,这种现象就称为侵入性。侵入性不利于程序后期的维护,尽可能避免。compare:比较*/
//        Collections.sort(list);//匿名内部类的形式创建一个比较器
//        Comparator<Point> com = new Comparator<Point>() {//            @Override
//            /**
//             * 实现比较器接口后必须重写方法compare.
//             * 该方法用来定义参数o1与参数o2的比较大小规则
//             * 返回值用来表示o1与o2的大小关系
//             */
//            public int compare(Point o1, Point o2) {//                int len1 = o1.getX() * o1.getX() + o1.getY() * o1.getY();
//                int len2 = o2.getX() * o2.getX() + o2.getY() * o2.getY();
//                return len1-len2;
//            }
//        };
//        Collections.sort(list,com);//回调模式//        Collections.sort(list,new Comparator<Point>() {//            public int compare(Point o1, Point o2) {//                int len1 = o1.getX() * o1.getX() + o1.getY() * o1.getY();
//                int len2 = o2.getX() * o2.getX() + o2.getY() * o2.getY();
//                return len1-len2;
//            }
//        });Collections.sort(list,(o1,o2)->o1.getX() * o1.getX() + o1.getY() * o1.getY() -o2.getX() * o2.getX() - o2.getY() * o2.getY());System.out.println(list);}
}
排序字符串

java中提供的类,如:String,包装类都实现了Comparable接口,但有时候这些比较规则不能满足我们的排序需求时,同样可以临时提供一种比较规则来进行排序.

package collection;import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;public class SortListDemo3 {public static void main(String[] args) {List<String> list = new ArrayList<>();
//        list.add("Tom");
//        list.add("jackson");
//        list.add("rose");
//        list.add("jill");
//        list.add("ada");
//        list.add("hanmeimei");
//        list.add("lilei");
//        list.add("hongtaoliu");
//        list.add("Jerry");list.add("传奇");list.add("小泽老师");list.add("苍老师");System.out.println(list);//按照字符多少排序
//        Collections.sort(list);
//        Collections.sort(list, new Comparator<String>() {//            public int compare(String o1, String o2) {                return o1.length()-o2.length();
//                return o2.length()-o1.length();//反过来减就是降序
//            }
//        });Collections.sort(list,(o1,o2)->o2.length()-o1.length());System.out.println(list);}
}
package map;import java.util.HashMap;
import java.util.Map;/*** java.util.Map接口 查找表* Map体现的结构是一个多行两列的表格,其中左列称为"key",右列称为"value"* Map总是以key-value对的形式保存一组数据。并且可以根据key获取对应的value。* Map有一个要求,key不允许重复(equals比较)** 常用实现类:* java.util.HashMap,我们称它为"散列表"。当今查询速度最快的数据结构。*/
public class MapDemo {public static void main(String[] args) {Map<String,Integer> map = new HashMap<>();/*V put(K k,V v)向当前Map中保存一组键值对*/map.put("语文",99);//如果Map的value是包装类类型,获取时不要用基本类型接收,避免自动拆箱引发空指针Integer value = map.put("数学",98);//key不存在时,put方法返回值为nullSystem.out.println("value:"+value);map.put("英语",97);map.put("物理",96);map.put("化学",99);System.out.println(map);value = map.put("数学",77);//key已经存在则替换value,返回值为key原来对应的valueSystem.out.println("value:"+value);System.out.println(map);/*V get(Object key)根据给定的key获取对应的value,如果给定的key不存在,则返回值为null*/value = map.get("语文");System.out.println("语文:"+value);value = map.get("体育");System.out.println("体育:"+value);//返回当前Map中的元素个数int size = map.size();System.out.println("size:"+size);/*删除给定的key对应的这组键值对,返回值为这个key对应的value*/value = map.remove("数学");System.out.println(map);System.out.println("value:"+value);//可以分别判定Map是否包含给定的key或value。判定依据仍然是equals方法。boolean ck = map.containsKey("英语");System.out.println("包含key:"+ck);boolean cv = map.containsValue(97);System.out.println("包含value:"+cv);}
}

map

package map;import java.util.*;/*** Map的遍历* Map支持三种遍历方式:* 1:单独遍历key* 2:遍历每一组键值对* 3:单独遍历value(这个操作不常用)*/
public class MapDemo2 {public static void main(String[] args) {Map<String,Integer> map = new HashMap<>();map.put("语文",99);map.put("数学",98);map.put("英语",97);map.put("物理",96);map.put("化学",99);System.out.println(map);/*Set keySet()将当前Map中所有的key以一个Set集合形式返回。*/Set<String> keySet = map.keySet();for(String key : keySet){System.out.println("key:"+key);}/*遍历每一组键值对Set entrySet()将当前Map中每一组键值对以Entry实例形式表示并存入集合后返将其返回。java.util.Map.Entry它的每一个实例用于表示一组键值对entry:条目*/Set<Map.Entry<String,Integer>> entrySet = map.entrySet();for(Map.Entry<String,Integer> e : entrySet){String key = e.getKey();Integer value = e.getValue();System.out.println(key+":"+value);}/*Collection values()将所有的value以一个集合的形式返回。*/Collection<Integer> values = map.values();for(Integer value : values){System.out.println("value:"+value);}/*JDK8之后集合和Map都支持了基于lambda表达式形式遍历。*///Map会将每一组键值对作为参数传给lambda表达式map.forEach((k,v)-> System.out.println(k+":"+v));Collection<String> c = new ArrayList<>();c.add("one");c.add("two");c.add("three");c.add("four");c.add("five");
//        c.forEach(
//                e->System.out.println(e)
//        );c.forEach(System.out::println);}
}

Java包装类,异常,集合,多线程,反射,IO,String类,lambda表达式,File类相关推荐

  1. Java学习记录五(多线程、网络编程、Lambda表达式和接口组成更新)

    Java学习记录五(多线程.网络编程.Lambda表达式和接口组成更新) Java 25.多线程 25.1实现多线程 25.1.1进程 25.1.2线程 25.1.3多线程的实现 25.1.4设置和获 ...

  2. 深圳Java培训学习:Java8.0新特性之Lambda表达式--【千锋】

    深圳Java培训学习:Java8.0新特性之Lambda表达式–[千锋] 前言 Java 8 已经发布很久了,很多报道表明Java 8 是一次重大的版本升级.本篇文章,主要给大家介绍的是lambda表 ...

  3. 【Java学习笔记之三十一】详解Java8 lambda表达式

    Java 8 发布日期是2014年3月18日,这次开创性的发布在Java社区引发了不少讨论,并让大家感到激动.特性之一便是随同发布的lambda表达式,它将允许我们将行为传到函数里.在Java 8之前 ...

  4. JAVA基础初探(十二)Map接口及其常用实现类(HashMap)、File类详解(概述、创建、删除、重命名、文件属性读取/设置、遍历文件夹)

    该篇博客目录 1.Map接口及其常用实现类(HashMap.Hashtable) 2.File类(概述.创建.删除.重命名.文件属性读取/设置.遍历文件夹) 一.Map接口及其常用实现类(HashMa ...

  5. Java中的集合多线程的理解

    Java中的集合 1.List.Set和Queue [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DwpgIu9t-1587300088779)(file:///C:\ ...

  6. 【Java】反射、枚举、Lambda表达式

    一.反射 1 定义 2 用途(了解) 3 反射基本信息 4 反射相关的类(重要) 4.1 Class类(反射机制的起源 ) 4.1.1 Class类中的相关方法(方法的使用方法在后边的示例当中) 4. ...

  7. Java IO流 序列二:File类

    2019独角兽企业重金招聘Python工程师标准>>> 2.File API 2.1. File类简介 包路径:java.io.File 功能:用于表示文件或者目录. 说明:File ...

  8. java 查找注解_Java利用反射如何查找使用指定注解的类详解

    前言 最近有些空,想自己写个跟spring里的注解一样的注解来用,然后希望能找到使用了自己写了注解的类,下面来介绍一下实现方法 声明,下面代码是没看过spring源码写的,基本上都是网上找的博客,整理 ...

  9. java的io流的file类_java IO流 (一) File类的使用

    1.File类的理解 * 1. File类的一个对象,代表一个文件或一个文件目录(俗称:文件夹) * 2. File类声明在java.io包下 * 3. File类中涉及到关于文件或文件目录的创建.删 ...

  10. Java基础知识(三十一)IO流(二) File类、递归、IO流基础

    目录 一. File类 1.File: 2.构造方法: 3.创建功能 4.删除功能 5.  重命名功能 6.判断功能 7.基本获取功能 8.高级获取功能 9.需求: 二.递归 1.概述: 2.   递 ...

最新文章

  1. php文章排序,PHP+Ajax实现后台文章快速排序
  2. Neural Representation Learning in NLP | 实录·PhD Talk #07
  3. 设置 docker容器 禁用网络
  4. 爬取动态网页(2017年年度豆瓣电影榜单)
  5. redis常用API
  6. 百度PARL再度夺冠NeurIPS仿生人挑战赛:强化学习控制的流畅行走
  7. python local variable_python学习笔记 - local, global and free variable
  8. C语言高级编程:二级指针的赋值
  9. C++学习之路 | PTA乙级—— 1006 换个格式输出整数 (15分)(精简)
  10. c#中索引器是什么_C#中的索引器
  11. spring boot定时任务解析
  12. python 单向链表
  13. 苹果充电配件MagSafe Duo Charger现已发售
  14. 计算机专业方向是什么意思,计算机专业就业方向是什么
  15. JS获取浏览器高度、屏幕高度、宽屏
  16. python人脸识别门禁系统_一种基于python的人脸识别开源系统
  17. 【字体分享】适合寒露闪屏设计的字体有哪些?
  18. 硬改路由器-MW310R-AR9341篇
  19. Excel VBA Dictionary excel vba 字典大全
  20. 【luogu CF633H】Fibonacci-ish II(莫队)(线段树)(矩阵乘法)

热门文章

  1. 弱监督学习综述-周志华(ML论文阅读笔记1)
  2. windows如何根据句柄hwnd显示和隐藏窗口
  3. 在VS2010中文版中配置OpenGL及问题解决
  4. python you-get ffmpeg 下载哔哩哔哩视频
  5. .Net Core Win2008R2 运行环境问题 502.5
  6. 工程制图与计算机绘图试卷A,工程制图与计算机绘图第4章
  7. matlab求解拉格朗日第一方程,matlab求解拉格朗日方程肿么编程
  8. 3D优化之ShadowGun系列二:浓烟,使用面片模拟粒子效果
  9. Rational Rose建立类图
  10. linux系统pdf文件转word文档,PDF如何转换为Word文件?用它,就是这么简单!