JAVA 基础 常用API合集字符串,正则,进制,File,IO,网络编程,线程,集合框架....

目录

JAVA API

一、JAVA API

1. 字符串String

2.正则表达式

3.进制

4.File类文件

5.IO(流)

6.java网络编程

7.线程

8.集合框架

JAVA API

一、JAVA API

1. 字符串String

  1. String:

    public class StringDemo {
        public static void main(String[] args) {
            /*
              常见面试题:
                String s = new String("hello");
                问:如上语句创建了几个对象?
                答:2个
                  第一个:字面量"hello"
                    ----java会创建一个String对象表示字面量"hello",并将其存入常量池
                  第二个:new String()
                    ----new String()时会再创建一个字符串对象,并引用hello字符串的内容
             */
    
            String s = new String("hello");
            String s1 = "hello";
            System.out.println("s:"+s);
            System.out.println("s1:"+s1);
            System.out.println(s==s1); //false,==比较的是地址是否相同
    
            //在实际应用中,String比较相等一般都是比较字符串内容是否相同
            //因此我们需要使用equals()方法来比较两个字符串内容是否相同
            System.out.println(s.equals(s1)); //true,equals()比较的是内容是否相同
    
            /*
            String s1 = "123abc"; //堆中有一个123abc字面量对象,同时常量池中缓存了
            //编译器在编译时,若发现是两个字面量连接,
            //则会直接连接好并将结果保存起来,如下语句相当于String s2 = "123abc";
            String s2 = "123"+"abc";
            System.out.println(s1==s2); //true,复用了常量池中的引用
    
            String s3 = "123";
            //因为s3是一个变量,所以在编译期并不会直接连接好
            String s4 = s3+"abc"; //创建一个新的对象存储123abc
            System.out.println(s4==s1); //false
             */
        }
    }

  2. String的常用方法:

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

      /**
       * int length():
       * 获取字符串的长度(字符个数)
       */
      public class LengthDemo {
          public static void main(String[] args) {
              String str = "我爱Java!";
              int len = str.length(); //获取str的长度
              System.out.println(len); //7
          }
      }

    • trim():去除当前字符串两边的空白字符

      /**
       * String trim():
       * 去除当前字符串两边的空白字符
       */
      public class TrimDemo {
          public static void main(String[] args) {
              String str = "     hello world     ";
              System.out.println(str); //     hello world
              str = str.trim(); //去除str两边的空白字符,并保存在str中
              System.out.println(str); //hello world
          }
      }

    • toUpperCase()/toLowerCase():将当前字符串中的英文部分给转换为全大写/全小写

      /**
       * String toUpperCase():
       * 将当前字符串中的英文部分给转换为全大写
       * String toLowerCase():
       * 将当前字符串中的英文部分给转换为全小写
       */
      public class ToUpperCaseDemo {
          public static void main(String[] args) {
              String str = "我爱Java!";
              String upper = str.toUpperCase(); //将str中英文部分转换为全大写,并存储在upper中
              System.out.println(upper); //我爱JAVA!
      ​
              String lower = str.toLowerCase(); //将str中英文部分转换为全小写,并存储在lower中
              System.out.println(lower); //我爱java!
          }
      }

    • startsWith/endsWith():判断当前字符串是否是以给定的字符串开始的/结束的

      /**
       * boolean startsWith(String s)
       * 判断当前字符串是否是以给定的字符串开始的
       * boolean endsWith(String s)
       * 判断当前字符串是否是以给定的字符串结束的
       */
      public class StartsWithDemo {
          public static void main(String[] args) {
              String str = "thinking in java"; //java编程思想(经典书)
              boolean starts = str.startsWith("think"); //判断str是否是以think开头的
              System.out.println(starts); //true
      ​
              boolean ends = str.endsWith(".png"); //判断str是否是以.png结尾的
              System.out.println(ends); //false
      ​
          }
      }

    • charAt():返回当前字符串指定位置上的字符-----根据位置找字符

      /**
       * char charAt(int index):
       * 返回当前字符串指定位置上的字符-----根据位置找字符
       */
      public class CharAtDemo {
          public static void main(String[] args) {
              //                      111111---和下面的连起来10/11/12/13/14/15
              //            0123456789012345
              String str = "thinking in java";
              char c = str.charAt(9); //获取str中下标9所对应的字符
              System.out.println(c); //i
          }
      }

    • indexOf()/lastIndexOf():检索给定字符串在当前字符串中的开始位置/最后一次出现位置

      /**
       * int indexOf(String s):
       * 检索给定字符串在当前字符串中的开始位置----根据字符串找对应位置
       * int lastIndexOf(String s):
       * 检索给定字符串在当前字符串中的最后一次出现的位置
       */
      public class IndexOfDemo {
          public static void main(String[] args) {
              //                      111111
              //            0123456789012345
              String str = "thinking in java";
              int index = str.indexOf("in"); //检索in在str中第1次出现的位置
              System.out.println(index); //2
              //从下标为3的位置开始找in第1次出现的位置
              index = str.indexOf("in",3);
              System.out.println(index); //5
              index = str.indexOf("WKJ"); //若字符串在str中不存在,则返回-1
              System.out.println(index); //-1
      ​
              index = str.lastIndexOf("in"); //找in最后一次出现的位置
              System.out.println(index); //9
      ​
          }
      }

    • substring():截取当前字符串中指定范围内的字符串

      /**
       * String substring(int start,int end):
       * 截取当前字符串中指定范围内的字符串(含头不含尾--包含start,但不包含end)
       */
      public class SubstringDemo {
          public static void main(String[] args) {
              //                      1
              //            01234567890
              String str = "www.tedu.cn";
              String name = str.substring(4,8); //截取下标4到7范围的字符串
              System.out.println(name); //tedu
      ​
              name = str.substring(4); //从下标4开始一直到末尾
              System.out.println(name); //tedu.cn
          }
      }

    • 静态方法valueOf():将其它数据类型转换为String

      /**
       * static String valueOf(数据类型 a):
       * 将其它数据类型转换为String
       */
      public class ValueOfDemo {
          public static void main(String[] args) {
              int a = 123;
              String s1 = String.valueOf(a); //将int型变量a转换为String类型并赋值给s1
              System.out.println(s1); //123---字符串类型
      
              double b = 123.456;
              String s2 = String.valueOf(b); //将double型变量b转换为String类型并赋值给s2
              System.out.println(s2); //123.456---字符串类型
      
              String s3 = b+""; //任何类型与字符串相连,结果都变为字符串类型,效率低(下节课讲)
              System.out.println(s3); //123.456---字符串类型
      
          }
      }

  3. StringBuilder:

    • 由于String是不变对象,每次修改内容都要创建新的对象,因此String不适合做频繁修改操作,为了解决这个问题,java提供了StringBuilder类。

    • StringBuilder是专门用于修改字符串的一个类,内部维护一个可变char数组,所做修改都是在这个数组上进行的,修改速度、性能非常优秀,并且提供了修改字符串的常见方法:增、删、改、插。

      public class StringStringBuilderDemo {
          public static void main(String[] args) {
              //用StringBuilder可以提高修改字符串的性能
              StringBuilder builder = new StringBuilder("a");
              for(int i=0;i<10000000;i++) { //1000万次
                  builder.append(i);
              }
              System.out.println("执行完毕");
      
              /*
              //String不适合频繁修改内容(效率低)
              String s = "a";
              for(int i=0;i<10000000;i++){ //1000万次
                  s = s+i; //每次修改都会在内存中分配新的对象
              }
              System.out.println("执行完毕");
              */
          }
      }

  4. StringBuilder的常用方法:

    • append():追加内容------------增

    • delete():删除部分内容--------删

    • replace():替换部分内容------改

    • insert():插入内容---------------插

      //StringBuilder的演示
      public class StringBuilderDemo {
          public static void main(String[] args) {
              String str = "好好学习Java";
              //复制str的内容到builder中-------好好学习java
              StringBuilder builder = new StringBuilder(str);
      
              //append():追加内容----在末尾追加
              builder.append(",为了找个好工作");
              System.out.println(builder); //好好学习java,为了找个好工作
      
              //replace():替换部分内容,含头不含尾
              builder.replace(9,16,"就是为了改变世界"); //将下标为9到15的内容替换为--就是为了改变世界
              System.out.println(builder); //好好学习java,就是为了改变世界
      
              //delete():删除部分内容,含头不含尾
              builder.delete(0,8); //删除0到7的
              System.out.println(builder); //,就是为了改变世界
      
              //insert():插入内容
              builder.insert(0,"活着"); //在下标为0的位置插入活着
              System.out.println(builder); //活着,就是为了改变世界
      		
              /*
              StringBuilder builder1 = new StringBuilder(); //空字符串
              StringBuilder builder2 = new StringBuilder("abc"); //abc串
              String str = "abc";
              StringBuilder builder3 = new StringBuilder(str); //abc串
      
              //将builder3转换为String类型
              String str2 = builder3.toString(); //toString()明天详细讲
              */
          }
      }

  1. 数组长度是length属性,字符串长度是length方法

  2. 字符串内容若做查看,那建议String-------------------------实际应用中一般都是查看

    字符串内容若需频繁修改,那建议StringBuilder

  3. StringBuilder和StringBuffer的区别:------现在先不要纠结,我先写着,学完线程之后才能理解

    • StringBuffer:线程安全的,同步处理的,性能稍慢

    • StringBuilder:非线程安全的,并发处理的,性能稍快-----一般都是用StringBuilder的

  4. getter/setter:

    //点
    public class Point { //很多框架都是基于getter/setter来取值、赋值的--一种习惯
        private int x;
        private int y;
    
        public int getX(){ //getter获取值
            return x;
        }
        public void setX(int x){ //setter设置值
            this.x = x;
        }
        public int getY(){
            return y;
        }
        public void setY(int y){
            this.y = y;
        }
    }
    
    //getter/setter的演示
    public class GetterSetterDemo {
        public static void main(String[] args) {
            Point p = new Point();
            p.setX(100); //赋值
            p.setY(200);
            System.out.println(p.getX()+","+p.getY());
        }
    }

  1. 正则表达式:

    • 用于描述字符串内容格式,使用它通常用于匹配一个字符串的内容是否符合格式要求

    • 正则表达式的语法:------------了解、不用纠结、不用深入研究

      1.[]:表示一个字符,该字符可以是[]中指定的内容
        例如:
        [abc]:这个字符可以是a或b或c
        [a-z]:表示任意一个小写字母
        [a-zA-Z]:表示任意一个字母
        [a-zA-Z0-9]:表示任意一个字母数字
        [a-zA-Z0-9_]:表示任意一个数字字母下划线
        [^abc]:该字符只要不是a或b或c
      2.预定义字符:
        .:表示任意一个字符,没有范围限制
        \d:表示任意一个数字,等同于[0-9]
        \w:表示任意一个单词字符,等同于[a-zA-Z0-9_]--单词字符指字母/数字/_
        \s:表示任意一个空白字符
        \D:表示不是数字
        \W:不是单词字符
        \S:不是空白字符
      3.量词:
        ?:表示前面的内容出现0-1次
          例如: [abc]? 可以匹配:a 或 b 或 c 或什么也不写
        +:表示前面的内容最少出现1次
          例如: [abc]+ 可以匹配:b或aaaaaaaaaa...或abcabcbabcbabcba....
                但是不能匹配:什么都不写 或 abcfdfsbbaqbb34bbwer...
        *:表示前面的内容出现任意次(0-多次)---匹配内容与+一致,只是可以一次都不写
          例如: [abc]* 可以匹配:b或aaaaaaaaaa...或abcabcba....或什么都不写
                但是不能匹配:abcfdfsbbaqbb34bbwer...
        {n}:表示前面的内容出现n次
            例如: [abc]{3} 可以匹配:aaa 或 bbb 或 aab 或abc 或bbc
                 但是不能匹配: aaaa 或 aad
        {n,m}:表示前面的内容出现最少n次最多m次  //下课--------------10:12继续
              例如: [abc]{3,5} 可以匹配:aaa 或  abcab 或者 abcc
              但是不能匹配:aaaaaa 或 aabbd
        {n,}:表示前面的内容出现n次以上(含n次)
             例如: [abc]{3,} 可以匹配:aaa 或 aaaaa.... 或 abcbabbcbabcba....
                  但是不能匹配:aa 或 abbdaw...
      4.()用于分组,是将括号内的内容看做是一个整体
        例如: (abc){3} 表示abc整体出现3次. 可以匹配abcabcabc
              但是不能匹配aaa 或abcabc
              (abc|def){3}表示abc或def整体出现3次.
              可以匹配: abcabcabc 或 defdefdef 或 abcdefabc
              但是不能匹配abcdef 或abcdfbdef
  2. String支持与正则表达式相关的方法:

    • matches():

    • replaceAll():

    • split():

  3. Object:对象/东西

    • 是所有类的鼻祖,所有类都直接或间接继承了Object,万物皆对象,为了多态

    • Object中有几个经常被派生类重写的方法:toString()和equals()

      • 调用Object类的toString()时默认返回:类的全称@地址,没有参考意义,所以常常重写toString()来返回具体属性的值

        注意:String、StringBuilder都重写toString()返回字符串内容了

      • 调用Object类的equals()时默认比较的还是==(即比较地址),没有参考意义,所以常常重写equals()来比较具体的属性值

        注意:

        1. String类重写equals()来比较字符串内容了

        StringBuilder并没重写equals(),所以依然还是调用Object的equals比较地址

        1. 重写equals()的基本原则:

          • 原则上要比较两个对象的属性是否相同

          • 两个对象必须是同一类型,若类型不同则返回false

  4. 包装类:

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

    • 包含:Integer、Character、Byte、Short、Long、Float、Double、Boolean。其中Character和Boolean是直接继承Object类的,而其余6个包装类继承自java.lang.Number类。

    • JDK1.5推出了一个新的特性:自动拆装箱。当编译器编译时若发现是基本类型与包装类型相互赋值,将自动补充代码来完成转换工作,这个过程称为自动拆装箱。

2.正则表达式

  1. 正则表达式:

    • 用于描述字符串内容格式,使用它通常用于匹配一个字符串的内容是否符合格式要求

    • 正则表达式的语法:------------了解、不用纠结、不用深入研究

      1.[]:表示一个字符,该字符可以是[]中指定的内容
        例如:
        [abc]:这个字符可以是a或b或c
        [a-z]:表示任意一个小写字母
        [a-zA-Z]:表示任意一个字母
        [a-zA-Z0-9]:表示任意一个字母数字
        [a-zA-Z0-9_]:表示任意一个数字字母下划线
        [^abc]:该字符只要不是a或b或c
      2.预定义字符:
        .:表示任意一个字符,没有范围限制
        \d:表示任意一个数字,等同于[0-9]
        \w:表示任意一个单词字符,等同于[a-zA-Z0-9_]--单词字符指字母/数字/_
        \s:表示任意一个空白字符
        \D:表示不是数字
        \W:不是单词字符
        \S:不是空白字符
      3.量词:
        ?:表示前面的内容出现0-1次
          例如: [abc]? 可以匹配:a 或 b 或 c 或什么也不写
        +:表示前面的内容最少出现1次
          例如: [abc]+ 可以匹配:b或aaaaaaaaaa...或abcabcbabcbabcba....
                但是不能匹配:什么都不写 或 abcfdfsbbaqbb34bbwer...
        *:表示前面的内容出现任意次(0-多次)---匹配内容与+一致,只是可以一次都不写
          例如: [abc]* 可以匹配:b或aaaaaaaaaa...或abcabcba....或什么都不写
                但是不能匹配:abcfdfsbbaqbb34bbwer...
        {n}:表示前面的内容出现n次
            例如: [abc]{3} 可以匹配:aaa 或 bbb 或 aab 或abc 或bbc
                 但是不能匹配: aaaa 或 aad
        {n,m}:表示前面的内容出现最少n次最多m次  //下课--------------10:12继续
              例如: [abc]{3,5} 可以匹配:aaa 或  abcab 或者 abcc
              但是不能匹配:aaaaaa 或 aabbd
        {n,}:表示前面的内容出现n次以上(含n次)
             例如: [abc]{3,} 可以匹配:aaa 或 aaaaa.... 或 abcbabbcbabcba....
                  但是不能匹配:aa 或 abbdaw...
      4.()用于分组,是将括号内的内容看做是一个整体
        例如: (abc){3} 表示abc整体出现3次. 可以匹配abcabcabc
              但是不能匹配aaa 或abcabc
              (abc|def){3}表示abc或def整体出现3次.
              可以匹配: abcabcabc 或 defdefdef 或 abcdefabc
              但是不能匹配abcdef 或abcdfbdef

  2. String支持与正则表达式相关的方法:

    • matches():使用给定的正则表达式(regex)验证当前字符串的格式是否符合要求,符合则返回true,否则返回false

      /**
       * boolean matches(String regex):匹配
       * 使用给定的正则表达式(regex)验证当前字符串的格式是否符合要求,符合则返回true,否则返回false
       */
      public class MatchesDemo {
          public static void main(String[] args) {
              /*
                邮箱的正则表达式:
                  [a-zA-Z0-9_]+@[a-zA-Z0-9]+(\.[a-zA-Z]+)+
               */
              String email = "wangkj@tedu.cn";
              String regex = "[a-zA-Z0-9_]+@[a-zA-Z0-9]+(\\.[a-zA-Z]+)+";
              //使用regex匹配email是否符合格式要求
              boolean match = email.matches(regex);
              if(match){
                  System.out.println("是正确的邮箱格式");
              }else{
                  System.out.println("不是正确的邮箱格式");
              }
          }
      }

    • replaceAll():将当前字符串中满足正则表达式(regex)的部分给替换为给定的字符串(s)

      /**
       * String replaceAll(String regex,String s):替换
       * 将当前字符串中满足正则表达式(regex)的部分给替换为给定的字符串(s)
       */
      public class ReplaceAllDemo {
          public static void main(String[] args) {
              String line = "abc123def456ghi78";
              //将line中的数字部分替换为#NUMBER#
              line = line.replaceAll("[0-9]+","#NUMBER#");
              System.out.println(line);
          }
      }

    • split():将当前字符串按照满足正则表达式的部分进行拆分,并将拆分出的以String[]形式来返回

      import java.util.Arrays;
      /**
       * String[] split(String regex):拆分
       * 将当前字符串按照满足正则表达式的部分进行拆分,并将拆分出的以String[]形式来返回
       */
      public class SplitDemo {
          public static void main(String[] args) {
              String line = "abc123def456ghi";
              String[] data = line.split("[0-9]+"); //按数字拆分(数字就拆没了)
              System.out.println(Arrays.toString(data)); //将data数组按String格式输出
      
              line = "123.456.78";
              data = line.split("\\."); //按.拆(.就拆没了)
              System.out.println(Arrays.toString(data));
      
              //最开始就是可拆分项(.),那么数组第1个元素为空字符串------------""
              //如果连续两个(两个以上)可拆分项,那么中间也会拆出一个空字符串----""
              //如果末尾连续多个可拆分项,那么拆出的空字符串被忽略
              line = ".123.456..78.......";
              data = line.split("\\."); //按.拆(.就拆没了)
              System.out.println(Arrays.toString(data));
          }
      }

  3. Object:对象/东西

    • 是所有类的鼻祖,所有类都直接或间接继承了Object,万物皆对象,为了多态

    • Object中有几个经常被派生类重写的方法:toString()和equals()

      • 调用Object类的toString()时默认返回:类的全称@地址,没有参考意义,所以常常重写toString()来返回具体属性的值

        注意:String、StringBuilder都重写toString()返回字符串内容了

        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;
            }
        
            @Override
            public String toString() {
                return "Point{" +
                        "x=" + x +
                        ", 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;
            }
        }
        
        public class ObjectDemo {
            public static void main(String[] args) {
                /*
                 输出引用变量时默认会调用Object的类toString()方法,
                 方法返回的字符串格式为:类的全称@地址
                 但通常这个返回结果对我们的开发没有帮助,
                 因此常常需要重写toString()返回自己想要的数据(一般都是属性值)
                 一旦Point类重写了toString()则会调用重写之后的
                 java中的String、StringBuilder等已经重写toString()返回字符串内容了
                 */
                Point p = new Point(100,200);
                System.out.println(p); //输出引用变量时默认调用Object类的toString()
                System.out.println(p.toString());
            }
        }

      • 调用Object类的equals()时默认比较的还是==(即比较地址),没有参考意义,所以常常重写equals()来比较具体的属性值

        注意:

        1. String类重写equals()来比较字符串内容了

        StringBuilder并没重写equals(),所以依然还是调用Object的equals比较地址

        1. 重写equals()的基本原则:

          • 原则上要比较两个对象的属性是否相同

          • 两个对象必须是同一类型,若类型不同则返回false

        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;
            }
        
            @Override
            public String toString() {
                return "Point{" +
                        "x=" + x +
                        ", y=" + y +
                        '}';
            }
        
            @Override
            public 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;
            }
        
            @Override
            public int hashCode() {
                return Objects.hash(x, 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;
            }
        }
        
        public class ObjectDemo {
            public static void main(String[] args) {
                /*
                  调用Object类的equals(),内部还是使用==来比较地址的,没有实际意义
                  若想比较对象的属性值相同,常常需要重写equals()
                  类一旦重写了equals(),则会调用重写之后的了
                 */
                /*
                Point p1 = new Point(100,200);
                Point p2 = new Point(100,200);
                System.out.println(p1==p2); //false,==比较的是地址
                //因为重写Point类的重写equals()中比较的是属性的值是否相同
                System.out.println(p1.equals(p2)); //true
                */
        
                String s1 = new String("hello");
                String s2 = new String("hello");
                //String重写equals()了
                System.out.println(s1.equals(s2)); //true,比较内容
        
                StringBuilder builder1 = new StringBuilder("hello");
                StringBuilder builder2 = new StringBuilder("hello");
                //StringBuilder没有重写equals()
                System.out.println(builder1.equals(builder2)); //false,比较地址
        
                //s1与builder1的类型不同,所以equals()一定是false
                System.out.println(s1.equals(builder1)); //false
            }
        }

  4. 包装类:

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

    • 包含:Integer、Character、Byte、Short、Long、Float、Double、Boolean。其中Character和Boolean是直接继承Object类的,而其余6个包装类继承自java.lang.Number类。

    • JDK1.5推出了一个新的特性:自动拆装箱。当编译器编译时若发现是基本类型与包装类型相互赋值,将自动补充代码来完成转换工作,这个过程称为自动拆装箱。

      public class IntegerDemo {
          public static void main(String[] args) {
              //演示包装类的常用操作:
              //1)可以通过包装类来得到基本类型的取值范围:
              int max = Integer.MAX_VALUE; //获取int的最大值
              int min = Integer.MIN_VALUE; //获取int的最小值
              System.out.println("int的最大值为:"+max+",最小值为:"+min);
              long max1 = Long.MAX_VALUE; //获取long的最大值
              long min1 = Long.MIN_VALUE; //获取long的最小值
              System.out.println("long的最大值为:"+max1+",最小值为:"+min1);
      
              //包装类型可以将字符串转换为对应的基本类型------必须熟练掌握
              String s1 = "38";
              int age = Integer.parseInt(s1); //将字符串s1转换为int类型
              System.out.println(age); //38------int
      
              String s2 = "123.456";
              double price = Double.parseDouble(s2); //将字符串s2转换为double类型
              System.out.println(price); //123.456------double
      
              /*
              //触发自动装箱特性,会被编译为: Integer i = Integer.valueOf(5);
              Integer i = 5; //基本类型到包装类型---装箱
      
              //触发自动拆箱特性,会被编译为: int j = i.intValue();
              int j = i; //包装类型到基本类型-------拆箱
              */
      
              /*
              Integer i1 = new Integer(5); //不常用
              Integer i2 = new Integer(5);
              System.out.println(i1==i2); //false,因为==是比较地址
      
              //Integer.valueOf()会复用-128到127范围内的数据---建议使用valueOf()方式
              Integer i3 = Integer.valueOf(5);
              Integer i4 = Integer.valueOf(5);
              System.out.println(i3==i4); //true
               */
          }
      }

3.进制

  1. 进制:

    1)十进制:
      1.1)规则:逢10进1
      1.2)数字:0 1 2 3 4 5 6 7 8 9
      1.3)基数:10
      1.4)权:万 千 百 十 个
    2)二进制:
      2.1)规则:逢2进1
      2.2)数字:0 1
      2.3)基数:2
      2.4)权:128 64 32 16 8 4 2 1
    3)十六进制:
      3.1)规则:逢16进1
      3.2)数字:0 1 2 3 4 5 6 7 8 9 a b c d e f
      3.3)基数:16
      3.4)权:65536 4096 256 16 1

    二进制转换为十进制的规则(正数):所有为1的权相加

    要求:今天必须熟练记住最后4个权(8 4 2 1)

    权:      32  16  8  4  2  1
    二进制:   1   1   0  1  0  1
    十进制:   32+16+4+1----------------53
        
    权:      32  16  8  4  2  1
    二进制:   0   0   1  1  0  1
    十进制:   8+4+1--------------------13
        
    权:      32  16  8  4  2  1
    二进制:   1   0   0  1  1  1
    十进制:   32+4+2+1-----------------39
  2. 十六进制的权:

    16的0次幂---------------1
    16的1次幂---------------16
    16的2次幂---------------256
    16的3次幂---------------4096
    16的4次幂---------------65536

    二进制的权:

    2的0次幂----------------1
    2的1次幂----------------2
    2的2次幂----------------4
    2的3次幂----------------8
    2的4次幂----------------16
    2的5次幂----------------32

    十进制的权:

    10的0次幂---------------1
    10的1次幂---------------10
    10的2次幂---------------100
    10的3次幂---------------1000
    10的4次幂---------------10000
  1. 什么是2进制:逢2进1的计数规则。计算机中的变量/常量都是按照2进制来计算的。

    • 2进制:

      • 规则:逢2进1

      • 数字:0 1

      • 基数:2

      • 权:128 64 32 16 8 4 2 1

    • 如何将2进制转换为10进制:

      • 将一个2进制每个1位置的权相加即可--------正数

        权:    32  16  8  4  2  1
        二进制:  1   0  1  1  0  1
        十进制: 32+8+4+1=45
  2. 什么是16进制:逢16进1的计数规则

    • 16进制:

      • 规则:逢16进1

      • 数字:0 1 2 3 4 5 6 7 8 9 a b c d e f

      • 基数:16

      • 权:4096 256 16 1

    • 用途:因为2进制书写太麻烦,所以常常用16进制来缩写2进制

    • 如何缩写:将2进制从低位开始,每4位2进制缩为1个16进制(每4位1缩)

      权:                  8 4 2 1
      2进制:   0010  1001  0010  1101  1110  1111  0110  1000
      16进制:   2     9     2     d     e     f     6      8----292def68
          
      权:                  8 4 2 1
      2进制:  0001 1100 1010 0011 1101
      16进制:  1    c    a    3    d---------1ca3d
          
      权:                  8 4 2 1
      2进制:   0011 1010 1011 1010 1011
      16进制:   3    a    b    a    b--------3abab
  3. 补码:-----------------了解,尽量多的吸收

    • 计算机处理有符号数(正负数)的一种编码方式

    • 以4位2进制为例讲解补码的编码规则:

      • 计算的时候如果超出4位则高位自动溢出舍弃,保持4位不变

      • 将4位2进制数分一半作为负数使用

      • 最高位称为符号位,高位为1是负数,高位为0是正数

    • 规律数:

      • 0111为4位补码的最大值,规律是1个0和3个1,可以推导出:

        • 32位补码的最大值:1个0和31个1------(011111111111......)

      • 1000为4位补码的最小值,规律是1个1和3个0,可以推导出:

        • 32位补码的最小值:1个1和31个0------(100000000000......)

      • 1111为4位补码的-1,规律是4个1,可以推导出:

        • 32位补码的-1:32个1--------(111111111111.....)

    • 深入理解负值:

      • 记住32位2进制数的-1的编码为:32个1

      • 用-1减去0位置对应的权----------负数

        1)11111111111111111111111111111111 = -1
        2)11111111111111111111111111111101 = -1-2= -3
        3)11111111111111111111111111111001 = -1-2-4= -7
        4)11111111111111111111111111111000 = -1-1-2-4= -8
        5)11111111111111111111111101101011 = -1-4-16-128= -149
        6)11111111111111111111111101101100 = -1-1-2-16-128 = -148
    • 互补对称现象:-n=~n+1-----取反+1

      -7    = 11111111111111111111111111111001 = -1-2-4=-1
      ~-7   = 00000000000000000000000000000110 = 2+4=6
      ~-7+1 = 00000000000000000000000000000111 = 1+2+4=7 
          
      5     = 00000000000000000000000000000101 = 1+4=5
      ~5    = 11111111111111111111111111111010 = -1-1-4=-6
      ~5+1  = 11111111111111111111111111111011 = -1-4=-5
          
      12    = 00000000000000000000000000001100 = 4+8=12
      ~12   = 11111111111111111111111111110011 = -1-4-8=-13
      ~12+1 = 11111111111111111111111111110100 = -1-1-2-8=-12
  4. 位运算:------------------------------了解

    • 取反:~

      • 0变1,1变0

    • 与运算:&

      • 运算规则:逻辑乘法,见0则0

        0 & 0 ---------> 0
        0 & 1 ---------> 0
        1 & 0 ---------> 0
        1 & 1 ---------> 1
    • 或运算:|

      • 运算规则:逻辑加法,见1则1

        0 | 0 ----------> 0
        0 | 1 ----------> 1
        1 | 0 ----------> 1
        1 | 1 ----------> 1
    • 右移位运算:>>>

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

    • 左移位运算:<<

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

    • 移位运算的数学意义:

4.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.txt
        File 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);
        }
    }
}

Lambda表达式

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

  • lambda可以用更精简的代码创建匿名内部类.但是该匿名内部类实现的接口只能有一个抽象方法,否则无法使用!

  • lambda表达式是编译器认可的,最终会将其改为内部类编译到class文件中

package lambda;

import java.io.File;
import java.io.FileFilter;

/**
 * JDK8之后java支持了lambda表达式这个特性
 * lambda表达式可以用更精简的语法创建匿名内部类,但是实现的接口只能有一个抽象
 * 方法,否则无法使用。
 * lambda表达式是编译器认可的,最终会被改为内部类形式编译到class文件中。
 *
 * 语法:
 * (参数列表)->{
 *     方法体
 * }
 */
public class LambdaDemo {
    public static void main(String[] args) {
        //匿名内部类形式创建FileFilter
        FileFilter filter = new FileFilter() {
            public boolean accept(File file) {
                return file.getName().startsWith(".");
            }
        };

        FileFilter filter2 = (File file)->{
            return file.getName().startsWith(".");
        };

        //lambda表达式中参数的类型可以忽略不写
        FileFilter filter3 = (file)->{
            return file.getName().startsWith(".");
        };

        /*
            lambda表达式方法体中若只有一句代码,则{}可以省略
            如果这句话有return关键字,那么return也要一并省略!
         */
        FileFilter filter4 = (file)->file.getName().startsWith(".");
    }
}

总结

File类

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

使用File可以做到:

  • 1:访问其表示的文件或目录的属性信息,例如:名字,大小,修改时间等等

  • 2:创建和删除文件或目录

  • 3:访问一个目录中的子项

常用构造器:

  • File(String pathname)

  • File(File parent,String name)可参考文档了解

常用方法:

  • length():返回一个long值,表示占用的磁盘空间,单位为字节。

  • canRead():File表示的文件或目录是否可读

  • canWrite():File表示的文件或目录是否可写

  • isHidden():File表示的文件或目录是否为隐藏的

  • createNewFile():创建一个新文件,如果指定的文件所在的目录不存在会抛出异常java.io.FileNotFoundException

  • mkdir:创建一个目录

  • mkdirs:创建一个目录,并且会将所有不存在的父目录一同创建出来,推荐使用。

  • delete():删除当前文件或目录,如果目录不是空的则删除失败。

  • exists():判断File表示的文件或目录是否真实存在。true:存在 false:不存在

  • isFile():判断当前File表示的是否为一个文件。

  • isDirectory():判断当前File表示的是否为一个目录

  • listFiles():获取File表示的目录中的所有子项

  • listFiles(FileFilter filter):获取File表示的目录中满足filter过滤器要求的所有子项

5.IO(流)

①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
 * IO:Input,Output 即:输入与输出
 *
 * JAVA IO用于我们程序可以和外界交换数据。用于与外界的数据进行读写操作的。
 *
 * java中将输入与输出比喻为"流":stream
 * 如何理解流:讲流想象为一个连接我们程序和另一端的"管道",在其中按照同一方向顺序移动的数据。
 *          有点像"水管"中向着统一方向流动的水。
 * 输入流:从外界向我们的程序中移动的方向,因此是用来获取数据的流,作用就是:读取操作
 * 输出流:写出操作
 * 注意:流是单向的,输入永远用来读,输出永远用来写。将来我们在实际开发中希望与程序交互的另一端
 * 互相发送数据时,我们只需要创建一个可以连接另一端的"流",进行读写操作完成。
 *
 * java定义了两个超类,来规范所有的字节流
 * java.io.InputStream:所有字节输入流的超类(抽象类),里面定义了读取字节的相关方法。
 *                     所有字节输入流都继承自它
 * java.io.OutputStream:所有字节输出流的超类(抽象类),里面定义了写出字节的相关方法。
 *                      所有的字节输出流都继承自它
 *
 *
 * 文件流
 * java.io.FileInputStream和java.io.FileOutputStream
 * 作用是真实连接我们程序和文件之间的"管道"。其中文件输入流用于从文件中读取字节。而文件输出流则
 * 用于向文件中写入字节。
 *
 * 文件流是节点流
 * JAVA IO将流划分为两类:节点流和处理流
 * 节点流:俗称"低级流",特点:真实连接我们程序和另一端的"管道",负责实际读写数据的流
 *       文件流就是典型的节点流,真实连接我们程序与文件的"管道",可以读写文件数据了。
 * 处理流:俗称"高级流"
 *       特点:
 *       1:不能独立存在(单独实例化进行读写操作不可以)
 *       2:必须连接在其他流上,目的是当数据"流经"当前流时,可以对其做某种加工操作,简化我们的工作、
 * 流的连接:实际开发中经常会串联一组高级流最终到某个低级流上,对数据进行流水线式的加工读写。
 *
 */
public class FOSDemo {
    public static void main(String[] args) throws IOException {
        //需求:向当前目录的文件fos.dat中写入数据
        /*
            在创建文件输出流时,文件输出流常见的构造器:
            FileOutputStream(String filename)
            FileOutputStream(File file)
            上述两种构造器都会在创建时将该文件创建出来(如果该文件不存在才会这样做),自动创建
            该文件的前提是该文件所在的目录必须存在,否则会抛出异常。
         */
        //        File file = new File("./fos.dat");
        //        FileOutputStream fos = new FileOutputStream(file);
        /*
            一个小技巧:在指定相对路径时,如果是从"当前目录"(./)开始的,那么"./"是可以忽略不写的
                      因为在相对路径中,默认就是从"./"开始
         */
//        FileOutputStream fos = new FileOutputStream("./fos.dat");
        FileOutputStream fos = new FileOutputStream("fos.dat");//与上面一句位置相同
​
        /*
            OutputStream(所有字节输出流的超类)中定义了写出字节的方法:
            write(int d)
            write(byte[] data)
            write(byte[] data,int offset,int len)
​
            其中:
            void write(int d)
            写出一个字节,将给定的参数int值对应的2进制的"低八位"写出。
​
            文件输出流继承OutputStream后就重写了该方法,作用是将该字节写入到文件中。
         */
​
        /*
            向文件中写入1个字节
            fow.write(1)
            将int值的1对应的2进制的"低八位"写如到文件第一个字节位置上
​
            1个int值占4个字节,每个字节是一个8为2进制
            int 1的2进制样子:
            00000000 00000000 00000000 00000001
                                       ^^^^^^^^
                                       写出的字节
​
            write方法调用后,fos.dat文件中就有了1个字节,内容为:
            00000001
​
            再次调用:
            fos.write(2)
            int 2的2进制样子:
            00000000 00000000 00000000 00000010
                                       ^^^^^^^^
                                       写出的字节
            write方法调用后,fos.dat文件中就有了2个字节,内容为:
            00000001 00000010
            上次写的   本次写的
         */
​
        fos.write(1);
        fos.write(2);
​
        System.out.println("写出完毕!");
        //注意!流使用完毕后要关闭,来释放底层资源
        fos.close();
​
​
    }
}

文件输入流

package io;
​
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
​
/**
 * 使用文件输入流读取文件中的数据
 */
public class FISDemo {
    public static void main(String[] args) throws IOException {
        //将fos.dat文件中的字节读取回来
        /*
            fos.dat文件中的数据:
            00000001 00000010
         */
        FileInputStream fis = new FileInputStream("fos.dat");
        /*
            java.io.InputStream(所有字节输入流的超类)定义着读取字节的相关方法
            int read()
            读取1个字节并以int型整数返回读取到的字节内容,返回的int值中对应的2进制的"低八位"
            就是读取到的数据。如果返回的int值为整数-1(这是一个特殊值,32位2进制全都是1)表达的
            是流读取到了末尾了。
​
            int read(byte[] data)
​
            文件输入流重写了上述两个方法用来从文件中读取对应的字节。
         */
​
        /*
             fos.dat文件中的数据:
             00000001 00000010
             ^^^^^^^^
           第一次读取的字节
​
             当我们第一次调用:
             int d = fis.read();//读取的是文件中第一个字节
​
             该int值d对应的2进制:
             00000000 00000000 00000000 00000001
             |------自动补充24个0-------| ^^^^^^^^
                                        读取到的数据
             而该2进制对应的整数就是1.
         */
        int d = fis.read();//读取到的就是整数1
        System.out.println(d);
        /*
            fos.dat文件中的数据:
             00000001 00000010
                      ^^^^^^^^
                     第二次读取的字节
​
             当我们第二次调用:
             d = fis.read();//读取的是文件中第二个字节
​
             该int值d对应的2进制:
             00000000 00000000 00000000 00000010
             |------自动补充24个0-------| ^^^^^^^^
                                        读取到的数据
             而该2进制对应的整数就是2.
         */
        d = fis.read();//2
        System.out.println(d);
​
        /*
            fos.dat文件中的数据:
             00000001 00000010 文件末尾
                               ^^^^^^^^
                              没有第三个字节
​
             当我们第三次调用:
             d = fis.read();//读取到文件末尾了!
​
             该int值d对应的2进制:
             11111111 11111111 11111111 11111111
             该数字是正常读取1个字节永远表达不了的值。并且-1的2进制格式好记。因此用它表达读取
             到了末尾。
​
         */
        d = fis.read();//-1
        System.out.println(d);
​
        fis.close();
    }
}
​

文件复制

package io;

import java.io.FileInputStream;
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");
    FileInputStream fis = new FileInputStream("01.rmvb");
    //用文件输出流向复制文件中写入复制的数据
//        FileOutputStream fos = new FileOutputStream("image_cp.jpg");
    FileOutputStream fos = new FileOutputStream("01_cp.rmvb");
        /*
            原文件image.jpg中的数据
            10100011 00111100 00001111 11110000....
            ^^^^^^^^
            读取该字节

            第一次调用:
            int d = fis.read();
            d的2进制:00000000 00000000 00000000 10100011
                                               读到的字节


            fos向复制的文件image_cp.jpg中写入字节

            第一次调用:
            fos.write(d);
            作用:将给定的int值d的2进制的"低八位"写入到文件中
            d的2进制:00000000 00000000 00000000 10100011
                                               写出字节
            调用后image_cp.jpg文件数据:
             10100011
         */
        /*
            循环条件是只要文件没有读到末尾就应该复制
            如何直到读取到末尾了呢?
            前提是:要先尝试读取一个字节,如果返回值是-1就说明读到末尾了
            如果返回值不是-1,则说明读取到的是一个字节的内容,就要将他写入到复制文件中
         */
    int d;//先定义一个变量,用于记录每次读取到的数据
    long start = System.currentTimeMillis();//获取当前系统时间
    while ((d = fis.read()) != -1) {
      fos.write(d);
    }
    long end = System.currentTimeMillis();
    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.FileOutputStream;
import java.io.IOException;

/**
 * 通过提高每次读写的数据,减少读写次数可以提高读写效率。
 */
public class CopyDemo2 {
  public static void main(String[] args) throws IOException {
    FileInputStream fis = new FileInputStream("01.rmvb");
    FileOutputStream fos = new FileOutputStream("01_cp.rmvb");
        /*
            块读:一次性读取一组字节
            块写:一次性将写出一组字节

            java.io.InputStream上定义了块读字节的方法:
            int read(byte[] data)
            一次性读取给定字节数组length个字节并从头开始装入到数组中。返回值为实际读取到的字节量
            如果返回值为-1则表示流读取到了末尾。
            文件流重写了该方法,作用是块读文件里的数据。

            java.io.OutputStream上定义了块写字节的方法:
            void write(byte[] data)
            一次性将给定的字节数组中所有的字节写出。

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


            原文件数据(假设文件共6个字节):
            11110000 00001111 01010101 11111111 00000000 10101010

            byte[] buf = new byte[4];//创建一个长度为4的字节数组
            buf默认的样子(每个元素若以2进制表现):{00000000,00000000,00000000,00000000}
            int len;//记录每次实际读取的字节数

            当第一次调用:
            len = fis.read(buf);
            由于字节数组buf的长度为4.因此可以一次性最多从文件中读取4个字节并装入到buf数组中
            返回值len表示的整数是这次实际读取到了几个字节。

            原文件数据(假设文件共6个字节):
            11110000 00001111 01010101 11111111 00000000 10101010
            ^^^^^^^^ ^^^^^^^^ ^^^^^^^^ ^^^^^^^^
                     第一次读取的4个字节

            buf:{11110000,00001111,01010101,11111111}
            len:4 表示本次读取到了4个字节


            第二次调用:
            len = fis.read(buf);

            原文件数据(假设文件共6个字节):
            11110000 00001111 01010101 11111111 00000000 10101010 文件末尾了
                                                ^^^^^^^^ ^^^^^^^^ ^^^^^^^^ ^^^^^^^^
                                                本次实际只能读取到2个字节
            buf:{00000000,10101010,01010101,11111111}
                 |本次新读的2字节数据| |---上次的旧数据---|
            len:2表示本次实际只读取到了2个字节。它的意义就是告诉你buf数组中前几个字节是本次真实
                读取到的数据


            第三次调用:
            len = fis.read(buf);
            原文件数据(假设文件共6个字节):
            11110000 00001111 01010101 11111111 00000000 10101010 文件末尾了
                                                                  ^^^^^^^^ ^^^^^^^^ ^^^^^^^^ ^^^^^^^^
            buf:{00000000,10101010,01010101,11111111} 没有任何变化!
            len:-1 表示本次读取时已经是文件末尾了!!
         */
        /*
            00000000 8位2进制 1byte  1字节
            1024byte = 1kb
            1024kb = 1mb
            1024mb = 1gb
            1024gb = 1tb
         */
        /*
            编译完该句代码:byte[] buf = new byte[10240];

            在实际开发中,有时候用一个计算表达式更能表现这个值的含义时,我们不妨使用计算表达式
            long t = 864000000;
            long t = 60 * 60 * 24 * 1000;

         */
    byte[] buf = new byte[1024 * 10];//10kb
    int len;//记录每次实际读取到的字节数
    long start = System.currentTimeMillis();
    while ((len = fis.read(buf)) != -1) {
      fos.write(buf, 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.nio.charset.StandardCharsets;

/**
 * 使用文件输出流向文件中写入文本数据
 */
public class WriteStringDemo {
    public static void main(String[] args) throws IOException {
        /*
            1:创建一个文件输出流
            2:将写出的文字先转换为2进制(一组字节)
            3:关闭流

            文件流有两种创建方式:
            1:覆盖模式,对应的构造器:
              FileOutputStream(String filename)
              FileOutputStream(File file)
              所谓覆盖模式:文件流在创建是若发现该文件已存在,则会将该文件原内容全部删除。然后
              在陆续将通过该流写出的内容保存到文件中。        
         */
        FileOutputStream fos = new FileOutputStream("fos.txt",true);
		String line = "让我再看你一遍,从南到北。";
        /*
            String提供了将内容转换为一组字节的方法:getBytes()
            java.nio.charset.StandardCharsets
         */
        byte[] data = line.getBytes(StandardCharsets.UTF_8);
        fos.write(data);

        line = "像是北五环路蒙住的双眼。";
        data = line.getBytes(StandardCharsets.UTF_8);
        fos.write(data);       

        System.out.println("写出完毕!");
        fos.close();
    }
}

文件输出流-追加模式

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

  • FileOutputStream(String path,boolean append)

  • FileOutputStream(File file,boolean append)

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

package io;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

/**
 * 使用文件输出流向文件中写入文本数据
 */
public class WriteStringDemo {
    public static void main(String[] args) throws IOException {
        /*
            1:创建一个文件输出流
            2:将写出的文字先转换为2进制(一组字节)
            3:关闭流

            文件流有两种创建方式:
            1:覆盖模式,对应的构造器:
              FileOutputStream(String filename)
              FileOutputStream(File file)
              所谓覆盖模式:文件流在创建是若发现该文件已存在,则会将该文件原内容全部删除。然后
              在陆续将通过该流写出的内容保存到文件中。

            2:追加模式,对应的构造器
              FileOutputStream(String filename,boolean append)
              FileOutputStream(File file,boolean append)
              当第二个参数为true时,那么就是追加模式。
              所谓追加模式:文件流在创建时若发现该文件已存在,则原内容都保留。通过当前流陆续写出
              的内容都会被陆续追加到文件末尾。
         */
        FileOutputStream fos = new FileOutputStream("fos.txt",true);

        String line = "斯国一!";
        byte[] data = line.getBytes(StandardCharsets.UTF_8);
        fos.write(data);

        line = "奥里给!";
        data = line.getBytes(StandardCharsets.UTF_8);
        fos.write(data);


        System.out.println("写出完毕!");
        fos.close();
    }
}

读取文本数据

package io;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

/**
 * 从文件中读取文本数据
 */
public class ReadStringDemo {
    public static void main(String[] args) throws IOException {
        /*
            1:创建一个文件输入流
            2:从文件中将字节都读取回来
            3:将读取到的字节转换回字符串
         */
        FileInputStream fis = new FileInputStream("fos.txt");

        byte[] data = new byte[1024];//1kb
        int len = fis.read(data);//块读操作,返回值表达实际读取到了多少字节
        System.out.println("实际读取了:"+len+"个字节");
        /*
            String提供了构造方法可以将一个字节数组还原为字符串
            String(byte[] data,Charset charset)
            将给定的字节数组data中所有字节按照给定的字符集转换为字符串。

            String(byte[] data,int offset,int len,Charset charset)
            将给定的字节数组data从下标offset处开始的连续len个字节按照指定的字符集转换为字符串
         */
        String line = new String(data,0,len,StandardCharsets.UTF_8);
        System.out.println(line.length());//输出字符串长度
        System.out.println(line);

        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;
import java.nio.charset.StandardCharsets;

/**
 * 缓冲输出流写出数据的缓冲区问题
 */
public class BOS_FlushDemo {
    public static void main(String[] args) throws IOException {
        FileOutputStream fos = new FileOutputStream("bos.txt");
        BufferedOutputStream bos = new BufferedOutputStream(fos);

        String line = "奥里给!";
        byte[] data = line.getBytes(StandardCharsets.UTF_8);
        bos.write(data);
        System.out.println("写出完毕!");
        /*
            缓冲流的flush方法用于强制将缓冲区中已经缓存的数据一次性写出。
            注:该方法实际上实在字节输出流的超类OutputStream上定义的,并非只有缓冲
            输出流有这个方法。但是实际上只有缓冲输出流的该方法有实际意义,其他的流实现
            该方法的目的仅仅是为了在流连接过程中传递flush动作给缓冲输出流。
         */
        bos.flush();//冲

        bos.close();

    }
}

对象流

java.io.ObjectOutputStream和ObjectInputSteam

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

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

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

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

package io;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

/**
 * 对象流(是一对高级流)
 * java.io.ObjectInputStream和ObjectOutputStream
 * 对象流在流连接中的作用是进行对象的序列化与反序列化
 * 其中对象输出流负责对象序列化。对象输入流负责对象反序列化
 *
 * 所谓对象序列化:
 * 将写出的对象按照其结构转换为一组字节的过程。
 */
public class OOSDemo {
    public static void main(String[] args) throws IOException {
        String name = "苍老师";
        int age = 55;
        String gender = "男";
        String[] otherInfo = {"摄影技术一流","喜欢拍片儿","是一名技术老师"};
        //将该Person对象写入文件person.obj中
        Person p = new Person(name,age,gender,otherInfo);

        FileOutputStream fos = new FileOutputStream("person.obj");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        /*
            对象输出流提供了一个直接写出对象的方法(进行对象序列化的操作)
            void writeObject(Object obj)

            序列化时可能出现异常:
            java.io.NotSerializableException: io.Person
            注:冒号后面的io.Person是指序列化的就是这个类的实例出现的错误
            原因:
            对象输出流在进行序列化对象时,要求该对象所属的类必须实现接口:java.io.Serializable接口
            并且该类中所有引用类型属性也必须实现该接口,否则会抛出上述异常。

         */
        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);
    }
}

需要进行序列化的类必须实现接口:java.io.Serializable 实现序列化接口后最好主动定义序列化版本号这个常量。 这样一来对象序列化时就不会根据类的结构生成一个版本号,而是使用该固定值。 那么反序列化时,只要还原的对象和当前类的版本号一致就可以进行还原。

transient关键字可以修饰属性,用于在进行对象序列化时忽略不必要的属性,达到对象瘦身的目的

package io;

import java.io.Serializable;
import java.util.Arrays;

/**
 * 使用当前类实例测试对象流的读写操作
 */
public class Person implements Serializable {
    public static final long serialVersionUID = 1L;
    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;
    }

    @Override
    public 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.*;
import java.nio.charset.StandardCharsets;

/**
 * JAVA IO将流按照读写数据的单位将流分为了两类:字节流与字符流
 * java.io.InputStream和OutputStream这两个超类是所有【字节流】的超类
 * java.io.Reader和Writer这两个是所有【字符流】的超类
 * 这两对超类之间是没有继承关系的,属于平级的。
 *
 * 字符流是以字符为最小单位(char)读写数据的。
 * 注:底层实际还是读写字节,只不过字符与字节的转换由字符流自动完成了。
 * 由于字符流最小读写单位为字符,因此字符流【只适合读写文本数据】
 *
 * 转换流(是一对高级流,同时是一对字符流)
 * 作用:
 * 1:衔接字节流与其他字符流
 * 2:将字符与字节相互转换
 * 实际开发中我们不会直接使用这一对流,但是在流连接中它是重要的一环。
 */
public class OSWDemo {
    public static void main(String[] args) throws IOException {
        /*
            使用这一对流演示转换流的读写字符方法

            java.io.Writer所有字符输出流的超类上,定义了写出字符的相关方法
            void write(int d)写出一个字符,实际传入的应当是一个char。
            void write(char[] data)
            void write(char[] data,int offset,int len)
            void write(String str) 直接写出一个字符串
         */
        FileOutputStream fos = new FileOutputStream("osw.txt");
        OutputStreamWriter osw = new OutputStreamWriter(fos,StandardCharsets.UTF_8);

        String line = "如果你突然打了个喷嚏,那一定就是我在想你。";
        osw.write(line);//转换流的write(String str)会将写出的字符串转换为字节然后写出
        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();
    }
}

转换流的意义:

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

③缓冲流

缓冲流是一对高级流,在流链接中链接它的目的是加快读写效率。缓冲流内部默认缓冲区为8kb,缓冲流总是块读写数据来提高读写效率

java.io.BufferedOutputStream缓冲字节输出流,继承自java.io.OutputStream

常用构造器

  • BufferedOutputStream(OutputStream out):创建一个默认8kb大小缓冲区的缓冲字节输出流,并连接到参数指定的字节输出流上。

  • BufferedOutputStream(OutputStream out,int size):创建一个size指定大小(单位是字节)缓冲区的缓冲字节输出流,并连接到参数指定的字节输出流上。

常用方法

flush():强制将缓冲区中已经缓存的数据一次性写出

缓冲流的写出方法功能与OutputStream上一致,需要知道的时write方法调用后并非实际写出,而是先将数据存入缓冲区(内部的字节数组中),当缓冲区满了时会自动写出一次。

java.io.BufferedInputStream缓冲字节输出流,继承自java.io.InputStream

常用构造器

  • BufferedInputStream(InputStream in):创建一个默认8kb大小缓冲区的缓冲字节输入流,并连接到参数指定的字节输入流上。

  • BufferedInputStream(InputStream in,int size):创建一个size指定大小(单位是字节)缓冲区的缓冲字节输入流,并连接到参数指定的字节输入流上。

常用方法

缓冲流的读取方法功能与InputStream上一致,需要知道的时read方法调用后缓冲流会一次性读取缓冲区大小的字节数据并存入缓冲区,然后再根据我们调用read方法读取的字节数进行返回,直到缓冲区所有数据都已经通过read方法返回后会再次读取一组数据进缓冲区。即:块读取操作

对象流

对象流是一对高级流,在流链接中的作用是完成对象的序列化反序列化

序列化:是对象输出流的工作,将一个对象按照其结构转换为一组字节的过程。

反序列化:是对象输入流的工作,将一组字节还原为对象的过程。

java.io.ObjectInputStream对象输入流,继承自java.io.InputStream

常用构造器

ObjectInputStream(InputStream in):创建一个对象输入流并连接到参数in这个输入流上。

常用方法

Object readObject():进行对象反序列化,将读取的字节转换为一个对象并以Object形式返回(多态)。

如果读取的字节表示的不是一个java对象会抛出异常:java.io.ClassNotFoundException

java.io.ObjectOutputStream对象输出流,继承自java.io.OutputStream

常用构造器

ObjectOutputStream(OutputStream out):创建一个对象输出流并连接到参数out这个输出流上

常用方法

void writeObject(Object obj):进行对象的序列化,将一个java对象序列化成一组字节后再通过连接的输出流将这组字节写出。

如果序列化的对象没有实现可序列化接口:java.io.Serializable就会抛出异常:java.io.NotSerializableException

序列化接口java.io.Serrializable

该接口没有任何抽象方法,但是只有实现了该接口的类的实例才能进行序列化与反序列化。

实现了序列化接口的类建议显示的定义常量:static final long serialVersionUID = 1L;

可以为属性添加关键字transient,被该关键字修饰的属性在序列化是会被忽略,达到对象序列化瘦身的目的。

字符流

java将流按照读写单位划分为字节与字符流。字节流以字节为单位读写,字符流以字符为单位读写。

转换流java.io.InputStreamReader和OutputStreamWriter

功能无需掌握,了解其核心意义:

1:衔接其它字节与字符流

2:将字符与字节进行转换

相当于是现实中的"转换器"

缓冲字符流

PrintWriter的自动行刷新功能

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

package io;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

/**
 * 练习PrintWriter的流连接操作
 */
public class PWDemo2 {
    public static void main(String[] args) throws FileNotFoundException {
        //文件输出流(低级流,字节流) 作用:向文件中写出字节
        FileOutputStream fos = new FileOutputStream("pw2.txt");
        //转换输出流(高级流,字符流) 作用:1衔接字符与字节流的 2:将写出的字符转换为字节
        OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8);
        //缓冲字符输出流(高级流,字符流) 作用:块写文本数据加速的(内部有一个8k的char数组)
        BufferedWriter bw = new BufferedWriter(osw);
        //具有自动行刷新功能(高级流,字符流) 作用:1按行写出字符串(println) 2:自动行刷新
        /*
            当我们创建PrintWriter时,构造方法里第一个参数为一个流,那么就支持再传入
            一个boolean值参数表示是否打开自动行刷新功能,传入true则打开。
            此时每当我们调用它的println方法写出一行字符串后就会自动flush()一次。
            注意:print方法和write方法写出字符串时并不会自动flush()!!!!
         */
        PrintWriter pw = new PrintWriter(bw,true);

        /*
            完成一个简易记事本工具
            将控制台上输入的每一行字符串按行写入到该文件中
            如果单独输入exit,则程序退出。
            思路:
            用一个死循环,重复做下面的工作
            1:在控制台上输入一行字符串
            2:判断输入的字符串是否为"exit"
              若是:则break掉循环退出程序
              若不是:则将输入的字符串通过println方法写入文件
         */
        Scanner scanner = new Scanner(System.in);
        while(true) {
           String line = scanner.nextLine();
           if("exit".equals(line)){
                break;
           }
           pw.println(line);
//           pw.print("");//不会自动flush
//           pw.write("");//不会自动flush
        }
        System.out.println("写出完毕!");

        pw.close();
    }
}

缓冲字符流

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

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

package io;

import java.io.*;

/**
 * 使用缓冲字符输入流按行读取字符串
 * 该高级流的主要作用:
 * 1:块读文本数据加速(内部有一个默认8k的char数组)
 * 2:可以按行读取字符串
 */
public class BRDemo {
    public static void main(String[] args) throws IOException {
        //将当前源代码输出到控制台上
        /*
            思路:
            读取当前源代码文件,按行读取,并且将读取到的每一行字符串都输出到控制台上即可
         */
        //文件输入流(低级流,字节流) 作用:从文件中读取字节
        FileInputStream fis = new FileInputStream("./src/io/BRDemo.java");
        //转换输入流(字符流,高级流) 作用:1衔接字节与字符流 2将读取的字节转换为字符
        InputStreamReader isr = new InputStreamReader(fis);
        //缓冲字符输入流(字符流,高级流) 作用:1块读字符数据加速  2按行读取字符串
        BufferedReader br = new BufferedReader(isr);
        /*
            BufferedReader缓冲字符输入流
            提供了一个独有的方法:readLine()
            作用:读取一行字符串。连续读取若干字符直到遇到了换行符位置,并将换行符之前的
                内容返回。注意:返回的字符串里不包含最后的换行符。
                特殊情况:
                如果这一行只有一个换行符,那么返回值为空字符串:""
                如果读取到了流的末尾,那么返回值为null。

            实际运行时:
            当我们第一次调用readLine()方法时,缓冲字符输入流实际会一次性读取8k的char
            回来并存入内部的char数组中(块读文本操作)。readLine方法只将char数组中从头
            开始一直到第一个换行符位置的内容以一个字符串形式返回。
         */
        String line;
        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){
 *     出现错误后的补救措施(B计划)
 * }
 */
public class TryCatchDemo {
    public static void main(String[] args) {
        System.out.println("程序开始了...");
        /*
            try{}语句块不能单独写,后面要么跟catch语句块要么跟finally语句块

            异常处理机制关注的是:明知道程序可能出现某种错误,但是该错误无法通过修改逻辑
            完全规避掉时,我们会使用异常处理机制,在出现该错误是提供一种补救办法。
            凡是能通过逻辑避免的错误都属于bug!就应当通过逻辑去避免!
         */
        try {
//            String str = null;
//            String str = "";
            String str = "a";
        /*
            若str=null的情况
            当JVM执行到下面代码时:str.length()会出现空指针,此时虚拟机就会根据该情况
            实例化一个对应的异常实例出来,即:空指针异常实例 NullPointerException实例
            然后将程序从一开始执行到报错这句话的过程设置到该异常实例中,此时该异常通过
            类型名字可以表达出现了什么错误,并将来可以通过输出错误信息来得知错误出现在那里
            虚拟机会将该异常抛出
            当某句代码抛出了一个异常时,JVM会做如下操作:
            1:检查报错这句话是否有被异常处理机制控制(有没有try-catch)
              如果有,则执行对应的catch操作,如果没有catch可以捕获该异常则视为没有
              异常处理动作
            2:如果没有异常处理,则异常会被抛出当当前代码所在的方法之外由调用当前方法的
              代码片段处理该异常
         */
            System.out.println(str.length());//抛出空指针异常
            System.out.println(str.charAt(0));
            System.out.println(Integer.parseInt(str));
            /*
                当try中某句代码报错后,就会跳出try执行下面对应的catch块,执行后就会
                退出catch继续向后执行。因此try语句块中报错代码以下的内容都不会被执行
             */
            System.out.println("!!!!!!!!!!!!!!");
//        }catch(NullPointerException e){
//            //这里实际开发中是写补救措施的,通常也会将异常信息输出便于debug
//            System.out.println("出现了空指针,并解决了!");
//        }catch(StringIndexOutOfBoundsException e){
//            System.out.println("处理字符串下标越界问题!");
//        }
        /*
            当try语句块中可能出现的几种不同异常对应的处理办法相同时,可以采取合并
            catch的做法,用同一个catch来捕获这几种可能出现的异常,而执行措施使用
            同一个。
         */
        }catch(NullPointerException|StringIndexOutOfBoundsException e){
            System.out.println("处理空指针或下标越界!");
        /*
            当catch捕获某个超类型异常时,那么try语句块中出现它类型异常时都可以被这个
            catch块捕获并处理。

            如果多个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块用于做释放资源这类操作,比如IO操作后的关闭流动作就非常适合在finally中进行
 */
public class FinallyDemo {
    public static void main(String[] args) {
        System.out.println("程序开始了...");
        try{
            String str = "abc";
            System.out.println(str.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接口,否则编译不通过.

package exception;
​
import java.io.FileOutputStream;
import java.io.IOException;
​
/**
 * JDK7之后java推出了一个特性:自动关闭特性
 * 旨在让我们用更简洁的语法完成IO操作的异常处理机制(主要就是简化了finally关闭流的操作)
 */
public class AutoCloseableDemo {
    public static void main(String[] args) {
        /*
            该特性是编译器认可的,并非虚拟机。实际上编译器编译完毕后的样子可参考FinallyDemo2
         */
        try(
                //只有实现了AutoCloseable接口的类才可以在这里定义!编译器最终会补充代码在finally中调用其close关闭
                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()throws IOException {}

    //允许不再抛出任何异常
//    public void dosome(){}

    //允许抛出超类方法抛出异常的子类型异常
//    public void dosome()throws FileNotFoundException {}

    //不允许抛出额外异常
//    public void dosome()throws SQLException {}

    //不允许抛出超类方法抛出异常的超类型异常
//    public void dosome()throws Exception{}
}

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 {}
}

6.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循环里面嵌套了一个内层循环(循环读取客户端发送消息),而循环执行机制决定了里层循环不结束,外层循环则无法进入第二次操作。

7.线程

重点:多线程并发安全问题

  • 什么是多线程并发安全问题:

    当多个线程并发操作同一临界资源,由于线程切换时机不确定,导致执行顺序出现混乱。

    解决办法:

    将并发操作改为同步操作就可有效的解决多线程并发安全问题

  • 同步与异步的概念:同步和异步都是说的多线程的执行方式。

    多线程各自执行各自的就是异步执行,而多线程执行出现了先后顺序进行就是同步执行

  • synchronized的两种用法

    1.直接在方法上声明,此时该方法称为同步方法,同步方法同时只能被一个线程执行

    2.同步块,推荐使用。同步块可以更准确的控制需要同步执行的代码片段。

    有效的缩小同步范围可以在保证并发安全的前提下提高并发效率

线程:一个顺序的单一的程序执行流程就是一个线程。代码一句一句的有先后顺序的执行。

多线程:多个单一顺序执行的流程并发运行。造成"感官上同时运行"的效果。

并发:

多个线程实际运行是走走停停的。线程调度程序会将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就是在守护线程上运行的.

多线程并发安全问题

当多个线程并发操作同一临界资源,由于线程切换时机不确定,导致操作临界资源的顺序出现混乱严重时可能导致系统瘫痪.
临界资源:操作该资源的全过程同时只能被单个线程完成.
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();
        }
    }
}

聊天室(续)

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

在服务端通过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();
            }
        }
    }
}

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

为了让能叫消息转发给所有客户端,我们 在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);
                }
            }
        }
    }
}

8.集合框架

什么是集合

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

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());//0
        System.out.println("是否为空集:"+c.isEmpty());
​
​
    }
}

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

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-8
        list.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));
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

闫氏开发

你的鼓励是我创作最大的动力!!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值