常见算法 简单认识算法
排序算法
冒泡排序 接下来,我们学习一种算法叫排序算法,它可以价格无序的整数,排列成从小到大的形式(升序),或者从大到小的形式(降序)
排序算法有很多种,我们这里只学习比较简单的两种,一种是冒泡排序,一种是选择排序。学习算法我们先要搞清楚算法的流程,然后再去“推敲“如何写代码。(注意,我这里用的次是推敲,也就是说算法这样的代码并不是一次成型的,是需要反复修改才能写好的 )。
先来学习冒泡排序,先来介绍一下,冒泡排序的流程
1 2 3 4 5 冒泡排序核心思路:每次将相邻的两个元素继续比较 如下图所示: 第一轮比较 3 次 第二轮比较 2 次 第三轮比较 1 次
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class Test1 { public static void main (String[] args) { int [] arr = {5 , 2 , 3 , 1 }; for (int i = 0 ; i < arr.length - 1 ; i++) { for (int j = 0 ; j < arr.length - i - 1 ; j++) { if (arr[j] > arr[j+1 ]){ int temp = arr[j + 1 ]; arr[j + 1 ] = arr[j]; arr[j] = temp; } } } System.out.println(Arrays.toString(arr)); } }
1.2 选择排序 接下来,按照选择排序的流程编写代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 ublic class Test2 { public static void main (String[] args) { int [] arr = {5 , 1 , 3 , 2 }; for (int i = 0 ; i < arr.length - 1 ; i++) { for (int j = i + 1 ; j < arr.length; j++) { if (arr[i] > arr[j]){ int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } } } System.out.println(Arrays.toString(arr)); } }
查找算法
先聊一聊基本查找: 假设我们要查找的元素是81,如果是基本查找的话,只能从0索引开始一个一个往后找,但是如果元素比较多,你要查找的元素比较靠后的话,这样查找的此处就比较多。性能比较差。
再讲二分查找 :二分查找的主要特点是,每次查找能排除一般元素,这样效率明显提高。但是二分查找要求比较苛刻,它要求元素必须是有序的,否则不能进行二分查找。
1 2 3 4 5 6 7 8 9 10 第1 步:先定义两个变量,分别记录开始索引(left)和结束索引(right) 第2 步:计算中间位置的索引,mid = (left+right)/2 ; 第3 步:每次查找中间mid位置的元素,和目标元素key进行比较 如果中间位置元素比目标元素小,那就说明mid前面的元素都比目标元素小 此时:left = mid+1 如果中间位置元素比目标元素大,那说明mid后面的元素都比目标元素大 此时:right = mid-1 如果中间位置元素和目标元素相等,那说明mid就是我们要找的位置 此时:把mid返回 注意:一搬查找一次肯定是不够的,所以需要把第1 步和第2 步循环来做,只到left>end就结束,如果最后还没有找到目标元素,就返回-1.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public class Test3 { public static void main (String[] args) { int [] arr = {7 , 23 , 79 , 81 , 103 , 127 , 131 , 147 }; System.out.println(binarySearch(arr, 150 )); System.out.println(Arrays.binarySearch(arr, 81 )); } public static int binarySearch (int [] arr, int data) { int left = 0 ; int right = arr.length - 1 ; while (left <= right){ int middle = (left + right) / 2 ; if (data < arr[middle]){ right = middle - 1 ; }else if (data > arr[middle]){ left = middle + 1 ; }else { return middle; } } return -1 ; } }
正则表达式 概述 接下来,我们学习一个全新的知识,叫做正则表达式。正则表达式其实是由一些特殊的符号组成的,它代表的是某种规则。
正则表达式的作用1:用来校验字符串数据是否合法
正则表达式的作用2:可以从一段文本中查找满足要求的内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public static boolean checkQQ (String qq) { if (qq == null || qq.startsWith("0" ) || qq.length() < 6 || qq.length() > 20 ){ return false ; } for (int i = 0 ; i < qq.length(); i++) { char ch = qq.charAt(i); if (ch < '0' || ch > '9' ){ return false ; } } return true ; }
1 2 3 public static boolean checkQQ1 (String qq) { return qq != null && qq.matches("[1-9]\\d{5,19}" ); }
书写规则
我们将这些规则,在代码中演示一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 public class RegexTest2 { public static void main (String[] args) { System.out.println("a" .matches("[abc]" )); System.out.println("e" .matches("[abcd]" )); System.out.println("d" .matches("[^abc]" )); System.out.println("a" .matches("[^abc]" )); System.out.println("b" .matches("[a-zA-Z]" )); System.out.println("2" .matches("[a-zA-Z]" )); System.out.println("k" .matches("[a-z&&[^bc]]" )); System.out.println("b" .matches("[a-z&&[^bc]]" )); System.out.println("ab" .matches("[a-zA-Z0-9]" )); System.out.println("徐" .matches("." )); System.out.println("徐徐" .matches("." )); System.out.println("\"" ); System.out.println("3" .matches("\\d" )); System.out.println("a" .matches("\\d" )); System.out.println(" " .matches("\\s" )); System.out.println("a" .matches("\s" )); System.out.println("a" .matches("\\S" )); System.out.println(" " .matches("\\S" )); System.out.println("a" .matches("\\w" )); System.out.println("_" .matches("\\w" )); System.out.println("徐" .matches("\\w" )); System.out.println("徐" .matches("\\W" )); System.out.println("a" .matches("\\W" )); System.out.println("23232" .matches("\\d" )); System.out.println("a" .matches("\\w?" )); System.out.println("" .matches("\\w?" )); System.out.println("abc" .matches("\\w?" )); System.out.println("abc12" .matches("\\w*" )); System.out.println("" .matches("\\w*" )); System.out.println("abc12张" .matches("\\w*" )); System.out.println("abc12" .matches("\\w+" )); System.out.println("" .matches("\\w+" )); System.out.println("abc12张" .matches("\\w+" )); System.out.println("a3c" .matches("\\w{3}" )); System.out.println("abcd" .matches("\\w{3}" )); System.out.println("abcd" .matches("\\w{3,}" )); System.out.println("ab" .matches("\\w{3,}" )); System.out.println("abcde徐" .matches("\\w{3,}" )); System.out.println("abc232d" .matches("\\w{3,9}" )); System.out.println("abc" .matches("(?i)abc" )); System.out.println("ABC" .matches("(?i)abc" )); System.out.println("aBc" .matches("a((?i)b)c" )); System.out.println("ABc" .matches("a((?i)b)c" )); System.out.println("abc" .matches("[a-z]{3}|\\d{3}" )); System.out.println("ABC" .matches("[a-z]{3}|\\d{3}" )); System.out.println("123" .matches("[a-z]{3}|\\d{3}" )); System.out.println("A12" .matches("[a-z]{3}|\\d{3}" )); System.out.println("我爱编程编程666666" .matches("我爱(编程)+(666)+" )); System.out.println("我爱编程编程66666" .matches("我爱(编程)+(666)+" )); } }
正则表达式应用案例 学习完正则表达式的规则之后,接下来我们再利用正则表达式,去校验几个实际案例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class RegexTest3 { public static void main (String[] args) { checkPhone(); } public static void checkPhone () { while (true ) { System.out.println("请您输入您的电话号码(手机|座机): " ); Scanner sc = new Scanner (System.in); String phone = sc.nextLine(); if (phone.matches("(1[3-9]\\d{9})|(0\\d{2,7}-?[1-9]\\d{4,19})" )){ System.out.println("您输入的号码格式正确~~~" ); break ; }else { System.out.println("您输入的号码格式不正确~~~" ); } } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class RegexTest3 { public static void main (String[] args) { checkEmail(); } public static void checkEmail () { while (true ) { System.out.println("请您输入您的邮箱: " ); Scanner sc = new Scanner (System.in); String email = sc.nextLine(); if (email.matches("\\w{2,}@\\w{2,20}(\\.\\w{2,10}){1,2}" )){ System.out.println("您输入的邮箱格式正确~~~" ); break ; }else { System.out.println("您输入的邮箱格式不正确~~~" ); } } } }
用于查找信息 正则表达式的第二个作用:在一段文本中查找满足要求的内容
用于搜索替换、分割内容
正则表达式用于搜索替换、分割内容,需要结合String提供的如下方法完成:
public string replaceAl1(String regex , string newStr) 按照正则表达式匹配的内容进行替换
public string[] split(String regex): 按照正则表达式匹配的内容进行分割字符串,反回一个字符串数组。
异常 认识异常
先来演示一个运行时异常产生
1 2 3 int [] arr = {11 ,22 ,33 };System.out.println(arr[5 ]);
再来演示一个编译时异常
我们在调用SimpleDateFormat对象的parse方法时,要求传递的参数必须和指定的日期格式一致,否则就会出现异常。 Java比较贴心,它为了更加强烈的提醒方法的调用者,设计了编译时异常,它把异常的提醒提前了,你调用方法是否真的有问题,只要可能有问题就给你报出异常提示(红色波浪线)。
编译时异常的目的:意思就是告诉你,这里小心点容易出错,仔细检查一下
有人说,我检查过了,我确认我的代码没问题,为了让它不报错,继续将代码写下去。我们这里有两种解决方案。
第一种:使用throws在方法上声明,意思就是告诉下一个调用者,这里面可能有异常啊,你调用时注意一下。
1 2 3 4 5 6 7 8 9 10 public class ExceptionTest1 { public static void main (String[] args) throws ParseException{ SimpleDateFormat sdf = new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss" ); Date d = sdf.parse("2028-11-11 10:24" ); System.out.println(d); } }
第二种:使用try…catch语句块异常进行处理。
1 2 3 4 5 6 7 8 9 10 11 public class ExceptionTest1 { public static void main (String[] args) throws ParseException{ try { SimpleDateFormat sdf = new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss" ); Date d = sdf.parse("2028-11-11 10:24" ); System.out.println(d); } catch (ParseException e) { e.printStackTrace(); } } }
好了,关于什么是异常,我们就先认识到这里。
自定义异常
自定义异常 Java无法为这个世界上全部的问题都提供异常类来代表,如果企业自己的某种问题,想通过异常来表示,以便用异常来管理该问题,那就需要自己来定义异常类了。
我们通过一个实际场景,来给大家演示自定义异常。
需求:写一个saveAge(int age)方法,在方法中对参数age进行判断,如果age<0或者>=150就认为年龄不合法,如果年龄不合法,就给调用者抛出一个年龄非法异常。
分析:Java的API中是没有年龄非常这个异常的,所以我们可以自定义一个异常类,用来表示年龄非法异常,然后再方法中抛出自定义异常即可。
先写一个异常类AgeIllegalException(这是自己取的名字,名字取得很奈斯),继承
1 2 3 4 5 6 7 8 9 public class AgeIllegalException extends Exception { public AgeIllegalException () { } public AgeIllegalException (String message) { super (message); } }
再写一个测试类,在测试类中定义一个saveAge(int age)方法,对age判断如果年龄不在0~150之间,就抛出一个AgeIllegalException异常对象给调用者。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class ExceptionTest2 { public static void main (String[] args) { try { saveAge2(225 ); System.out.println("saveAge2底层执行是成功的!" ); } catch (AgeIllegalException e) { e.printStackTrace(); System.out.println("saveAge2底层执行是出现bug的!" ); } } public static void saveAge (int age) { if (age > 0 && age < 150 ){ System.out.println("年龄被成功保存: " + age); }else { throw new AgeIllegalRuntimeException ("/age is illegal, your age is " + age); } } }
注意咯,自定义异常可能是编译时异常,也可以是运行时异常
1 2 3 4 5 1. 如果自定义异常类继承Excpetion,则是编译时异常。 特点:方法中抛出的是编译时异常,必须在方法上使用throws 声明,强制调用者处理。 2. 如果自定义异常类继承RuntimeException,则运行时异常。 特点:方法中抛出的是运行时异常,不需要在方法上用throws 声明。
自定义运行时异常
定义一个异常类继承RuntimeException.
重写构造器。
通过throw new异常类(xxx)来创建异常对象并抛出。 编译阶段不报错,提醒不强烈,运行时才可能出现!!
自定义编译时异常
定义一个异常类继承Exception.
重写构造器。
通过throw new异常类(xoxx)来创建异常对象并抛出。 编译阶段就报错,提醒更加强烈!
异常是用来查寻系统Bug的关键参考信息!
异常示以作为方法内部的一种特殊返回值,以便通知上层调用者底层的执行情况!
异常的处理
开发中对于异常的常见处理方式 方法A 方法B 异常,抛给调用者 方法C 异常,抛给调用者
我们看一个代码,main方法调用test1方法,test1方法调用test2方法,test1和test2方法中多有扔异常。
第一种处理方式是,在main方法中对异常进行try…catch捕获处理了,给出友好提示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class ExceptionTest3 { public static void main (String[] args) { try { test1(); } catch (FileNotFoundException e) { System.out.println("您要找的文件不存在!!" ); e.printStackTrace(); } catch (ParseException e) { System.out.println("您要解析的时间有问题了!" ); e.printStackTrace(); } } public static void test1 () throws FileNotFoundException, ParseException { SimpleDateFormat sdf = new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss" ); Date d = sdf.parse("2028-11-11 10:24:11" ); System.out.println(d); test2(); } public static void test2 () throws FileNotFoundException { InputStream is = new FileInputStream ("D:/meinv.png" ); } }
第二种处理方式是:在main方法中对异常进行捕获,并尝试修复
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class ExceptionTest4 { public static void main (String[] args) { while (true ) { try { System.out.println(getMoney()); break ; } catch (Exception e) { System.out.println("请您输入合法的数字!!" ); } } } public static double getMoney () { Scanner sc = new Scanner (System.in); while (true ) { System.out.println("请您输入合适的价格:" ); double money = sc.nextDouble(); if (money >= 0 ){ return money; }else { System.out.println("您输入的价格是不合适的!" ); } } } }
捕获异常,记录异常并响应合适的信息给用户
捕获异常,尝试重新修复
集合框架 概述
集合体系结构 Collection Map 单列集合 双列集合
collection代表单列集合,每个元素(数据)只包含一个值。
Map代表双列集合,每个元素包含两个值(键值对)。
Collection集合体系
各种集合的特点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ArrayList<String> list = new ArrayList <>(); list.add("java1" ); list.add("java2" ); list.add("java1" ); list.add("java2" ); System.out.println(list); HashSet<String> list = new HashSet <>(); list.add("java1" ); list.add("java2" ); list.add("java1" ); list.add("java2" ); list.add("java3" ); System.out.println(list);
Collection的常用方法
Collection是单列集合的祖宗,它规定的方法(功能)是全部单列集合都会继承的。collection的常见方法如下:
public boolean add(E e) 把给定的对象添加到当前集合中 public void clear() 清空集合中所有的元素 public boolean remove(E e) 把给定的对象在当前集合中删除 public boolean contains(object obj) 判断当前集合中是否包含给定的对象 public boolean isEmpty() 判断当前集合是否为空 public int size() 返回集合中元素的个数。 public object[ ] toArray() 把集合中的元素,存储到数组中
Collection集合的一些常用方法,这些方法所有Collection实现类都可以使用。 这里我们以创建ArrayList为例,来演示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 Collection<String> c = new ArrayList <>(); c.add("java1" ); c.add("java1" ); c.add("java2" ); c.add("java2" ); c.add("java3" ); System.out.println(c); System.out.println(c.size()); System.out.println(c.contains("java1" )); System.out.println(c.contains("Java1" )); System.out.println(c.remove("java1" )); System.out.println(c); c.clear(); System.out.println(c); System.out.println(c.isEmpty()); Object[] array = c.toArray(); System.out.println(Arrays.toString(array)); String[] array1 = c.toArray(new String [c.size()]); System.out.println(Arrays.toString(array1)); Collection<String> c1 = new ArrayList <>(); c1.add("java1" ); c1.add("java2" ); Collection<String> c2 = new ArrayList <>(); c2.add("java3" ); c2.add("java4" ); c1.addAll(c2); System.out.println(c1);
最后,我们总结一下Collection集合的常用功能有哪些,ArrayList、LinkedList、HashSet、LinkedHashSet、TreeSet集合都可以调用下面的方法。
Collection的遍历方式 迭代器
迭代器概述 迭代器是用来遍历集合的专用方式(数组没有迭代器),在Java中迭代器的代表是lterator。
Collection集合获取迭代器的方法 Iterator iterator() 返回集合中的迭代器对象,该迭代器对象默认指向当前集合的第一个元素
lterator迭代器中的常用方法 boolean hasNext() 询问当前位置是否有元紊存在,存在返回true ,不存在返回false E next() 获取当前位置的元素,并同时将迭代器对象指向下一个元素处。
接下来学习的迭代器就是一种集合的通用遍历方式。
代码写法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Collection<String> c = new ArrayList <>(); c.add("赵敏" ); c.add("小昭" ); c.add("素素" ); c.add("灭绝" ); System.out.println(c); Iterator<String> it = c.iterator(); while (it.hasNext()){ String e = it.next(); System.out.println(s); }
迭代器代码的原理如下:
当调用iterator()方法获取迭代器时,当前指向第一个元素
hasNext()方法则判断这个位置是否有元素,如果有则返回true,进入循环
调用next()方法获取元素,并将当月元素指向下一个位置,
等下次循环时,则获取下一个元素,依此内推
增强for
增强for循环 格式: for(元素的数据类型变量名:数组或者集合){ }1 2 3 4 5 Collection<String> c = new ArrayList <>(); ... for (String s : c){System.out.println(s); }
增强for可以用来遍历集合或者数组。
增强for遍历集合,本质就是迭代器遍历集合的简化写法。
forEach遍历集合 在JDK8版本以后还提供了一个forEach方法也可以遍历集合,如果下图所示:
我们发现forEach方法的参数是一个Consumer接口,而Consumer是一个函数式接口,所以可以传递Lambda表达式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Collection<String> c = new ArrayList <>(); c.add("赵敏" ); c.add("小昭" ); c.add("素素" ); c.add("灭绝" ); c.forEach(new Consumer <String>{ @Override public void accept (String s) { System.out.println(s); } }); c.forEach(s->System.out.println(s));
遍历集合案例 接下来,我们看一个案例,在集合中存储自定义的对象,并遍历。具体要求如下
首先,我们得写一个电影类,用来描述每一步电影应该有哪些信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Movie { private String name; private double score; private String actor; public Movie () {} public Movie (String name, double score, String actor) { this .name=name; this .score=score; this .actor=actor; } }
接着,再创建一个测试类,完成上面的需求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Test { public static void main (String[] args) { Collection<Movie> movies = new ArrayList <>(); movies.add(new MOvie ("《肖申克的救赎》" , 9.7 , "罗宾斯" )); movies.add(new MOvie ("《霸王别姬》" , 9.6 , "张国荣、张丰毅" )); movies.add(new MOvie ("《阿甘正传》" , 9.5 , "汤姆汉克斯" )); for (Movie movie : movies){ System.out.println("电影名:" + movie.getName()); System.out.println("评分:" + movie.getScore()); System.out.println("主演:" + movie.getActor()); } } }
以上代码的内存原理如下图所示:当往集合中存对象时,实际上存储的是对象的地址值
List集合 特点、特有方式
List集合因为支持索引,所以多了很多与索引相关的方法,当然,Collection的功能List也都继承了。
方法名称 说明 void add(int index,E element) 在此集合中的指定位置插入指定的元素 E remove(int index) 删除指定索引处的元素,返回被删除的元素 E set(int index,E element) 修改指定索引处的元素,返回被修改的元素 E get(int index) 返回指定索引处的元素
List集合是索引的,所以多了一些有索引操作的方法,如下图所示:
接下来,我们用代码演示一下这几个方法的效果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 List<String> list = new ArrayList <>(); list.add("蜘蛛精" ); list.add("至尊宝" ); list.add("至尊宝" ); list.add("牛夫人" ); System.out.println(list); list.add(2 , "紫霞仙子" ); System.out.println(list); System.out.println(list.remove(2 )); System.out.println(list); System.out.println(list.get(3 )); System.out.println(list.set(3 ,"牛魔王" )); System.out.println(list);
遍历方式
for循环(因为List集合有索引)
迭代器
增强for循环
Lambda表达式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 List<String> list = new ArrayList <>(); list.add("蜘蛛精" ); list.add("至尊宝" ); list.add("糖宝宝" ); for (int i = 0 ; i< list.size(); i++){ String e = list.get(i); System.out.println(e); } for (String s : list){ System.out.println(s); } Iterator<String> it = list.iterator(); while (it.hasNext()){ String s = it.next(); System.out.println(s); } list.forEach(s->System.out.println(s));
ArrayList集合的底层原理
ArrayList集合的底层原理
利用无参构造器创建的集合,会在底层创建一个默认长度为0的数组
添加第一个元素时,底层会创建一个新的长度为10的数组
存满时,会扩容1.5倍
如果一次添加多个元素,1.5倍还放不下,则新创建数组的长度以实际为准
查询速度快(注意∶是根据索引查询数据快)︰查询数据通过地址值和索引定位,查询任意数据耗时相同。
删除效率低:可能需要把后面很多的数据进行前移。
添加效率极低:可能需要把后面很多的数据后移,再添加元素;或者也可能需要进行数组的扩容。
ArrayList集合话合的应用场景
1、ArrayList适合:根据索引查询数据, 比如根据随机索查询数据(高效)! 或考数据量不是很大时! 2、ArrayList不适合:数据量大的同时,又要频繁的进行增删操作!
LinkList集合的底层原理
基于双链表实现的。
特点:查询慢,增删相对较快,但对首尾元素进行增删改查的速度是极快的。
LinkedList新增了:很多首尾操作的特有方法。 方法名称 说明 public void addFirst(E e) 在该列表开头插入指定的元素 public void addLast(E e) 将指定的元素追加到此列表的末尾 public E getFirst() 返回此列表中的第一个元素 public E getLast() 返回此列表中的最后一个元素 public E removeFirst() 从此列表中删除并返回第一个元素 public E removeLast() 从此列表中删除并返回最后一个元素
LinkedList的应用场景之一:可以用来设计队列 只是在首尾增册删元素,用LinkedList来实现很合话!
入队 出队先进先出,后进后出
LinkedList的应用场景之一:可以用来设计栈 只是在首部增删元素,用LinkedList来实现很合话!
栈顶元素 数据进入栈模型的过程称为:压/进栈(push) 数据离开栈模型的过程称为:弹/出栈(pop) 栈底元素
后进先出,先进后出
Set集合 特点
Set系列集合特点:无序:添加数据的顺序和获取出的数据顺序不一致;不重复;无索引; HashSet:无序、不重复、无索引。 LinkedHashSet:有序、不重复、无索引。 TreeSet排序、不重复、无索引。
下面我们用代码简单演示一下,每一种Set集合的特点。
1 2 3 4 5 6 7 8 9 10 11 Set<Integer> set = new TreeSet <>(); set.add(666 ); set.add(555 ); set.add(555 ); set.add(888 ); set.add(888 ); set.add(777 ); set.add(777 ); System.out.println(set);
HashSet集合的底层原理
就是一个int类型的数值,Java中每个对象都有一个哈希值。
Java中的所有对象,都可以调用Obejct类提供的hashCode方法,返回该对象自己的哈希值。 public int hashCode(): 返回对象的哈希码值。
同一个对象多次调用hashCode()方法返回的哈希值是相同的。
不同的对象,它们的哈希值一般不相同,但也有可能会相同(哈希碰撞)。 int (-21亿多~21亿多) 45亿个对象
HashSet集合默认不能对内容一样的两个不同对象去重复! 比如内容一样的两个学生对象存入到HashSet集合中去,HashSet集合是不能去重复的!
HashSet存储元素的原理,依赖于两个方法:一个是hashCode方法用来确定在底层数组中存储的位置,另一个是用equals方法判断新添加的元素是否和集合中已有的元素相同。
要想保证在HashSet集合中没有重复元素,我们需要重写元素类的hashCode和equals方法。比如以下面的Student类为例,假设把Student类的对象作为HashSet集合的元素,想要让学生的姓名和年龄相同,就认为元素重复。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public class Student { private String name; private int age; private double height; public Student () {} public Student (String name, int age, double height) { this .name=name; this .age=age; this .height=height; } @Override public boolean equals (Object o) { if (this == o) return true ; if (o == null || getClass() != o.getClass()) return false ; Student student = (Student) o; if (age != student.age) return false ; if (Double.compare(student.height, height) != 0 ) return false ; return name != null ? name.equals(student.name) : student.name == null ; } @Override public int hashCode () { int result; long temp; result = name != null ? name.hashCode() : 0 ; result = 31 * result + age; temp = Double.doubleToLongBits(height); result = 31 * result + (int ) (temp ^ (temp >>> 32 )); return result; } }
接着,写一个测试类,往HashSet集合中存储Student对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class Test { public static void main (String[] args) { Set<Student> students = new HashSet <>(); Student s1 = new Student ("至尊宝" ,20 , 169.6 ); Student s2 = new Student ("蜘蛛精" ,23 , 169.6 ); Student s3 = new Student ("蜘蛛精" ,23 , 169.6 ); Student s4 = new Student ("牛魔王" ,48 , 169.6 ); students.add(s1); students.add(s2); students.add(s3); students.add(s4); for (Student s : students){ System.out.println(s); } } }
打印结果如下,我们发现存了两个蜘蛛精,当时实际打印出来只有一个,而且是无序的。
1 2 3 Student{name='牛魔王' , age=48 , height=169.6 } Student{name='至尊宝' , age=20 , height=169.6 } Student{name='蜘蛛精' , age=23 , height=169.6 }
LinkedHashSet集合的底层原理
依然是基于哈希表(数组、链表、红黑树)实现的。
但是,它的每个元素都额外的多了一个双链表的机制记录它前后元素的位置。
TreeSet集合
特点:不重复、无索引、可排序(默认升序排序,按照元素的大小,由小到大排序)
底层是基于红黑树实现的排序。
对于数值类型:Integer , Double,默认按照数值本身的大小进行升序排序。
对于字符串类型:默认按照首字符的编号升序排序。
对于自定义类型如Student对象,TreeSet默认是无法直接排序的。
TreeSet集合的特点是可以对元素进行排序,但是必须指定元素的排序规则。
如果往集合中存储String类型的元素,或者Integer类型的元素,它们本身就具备排序规则,所以直接就可以排序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Set<Integer> set1= new TreeSet <>(); set1.add(8 ); set1.add(6 ); set1.add(4 ); set1.add(3 ); set1.add(7 ); set1.add(1 ); set1.add(5 ); set1.add(2 ); System.out.println(set1); Set<Integer> set2= new TreeSet <>(); set2.add("a" ); set2.add("c" ); set2.add("e" ); set2.add("b" ); set2.add("d" ); set2.add("f" ); set2.add("g" ); System.out.println(set1);
如果往TreeSet集合中存储自定义类型的元素,比如说Student类型,则需要我们自己指定排序规则,否则会出现异常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Set<Student> students = new TreeSet <>(); Student s1 = new Student ("至尊宝" ,20 , 169.6 );Student s2 = new Student ("紫霞" ,23 , 169.8 );Student s3 = new Student ("蜘蛛精" ,23 , 169.6 );Student s4 = new Student ("牛魔王" ,48 , 169.6 );students.add(s1); students.add(s2); students.add(s3); students.add(s4); System.out.println(students);
此时运行代码,会直接报错。原因是TreeSet不知道按照什么条件对Student对象来排序。
自定义排序规则 TreeSet集合存储自定义类型的对象时,必须指定排序规则,支持如下两种方式来指定比较规则。
第一种:让元素的类实现Comparable接口,重写compareTo方法
第二种:在创建TreeSet集合时,通过构造方法传递Compartor比较器对象
public TreeSet(Comparator<? super E> comparator)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class Student implements Comparable <Student>{ private String name; private int age; private double height; public Student () {} public Student (String name, int age, double height) { this .name=name; this .age=age; this .height=height; } @Override public int compareTo (Student o) { return this .age-o.age; } }
此时,再运行测试类,结果如下
1 2 3 4 Student{name='至尊宝' , age=20 , height=169.6 } Student{name='紫霞' , age=20 , height=169.8 } Student{name='蜘蛛精' , age=23 , height=169.6 } Student{name='牛魔王' , age=48 , height=169.6 }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 Set<Student> students = new TreeSet <>(new Comparator <Student>{ @Override public int compare (Student o1, Student o2) { return Double.compare(o1,o2); } }); Student s1 = new Student ("至尊宝" ,20 , 169.6 );Student s2 = new Student ("紫霞" ,23 , 169.8 );Student s3 = new Student ("蜘蛛精" ,23 , 169.6 );Student s4 = new Student ("牛魔王" ,48 , 169.6 );students.add(s1); students.add(s2); students.add(s3); students.add(s4); System.out.println(students);
如果认为第一个元素>第二个元素返回正整数即可。
如果认为第一个元素<第二个元素返回负整数即可。
如果认为第一个元素=第二个元素返回0即可,此时TreeSet集合只会保留一个元素,认为两者重复。 注意:如果类本身有实现Comparable接口,TreeSet集合同时也自带比较器,默认使用集合自带的比较器排序。
注意事项:集合的并发修改异常问题
如果希望记住元素的添加顺序,需要存储重复的元素,又要频繁的根据索引查询数据? 用ArrayList集合(有序、可重复、有索引),底层基于数组的。(常用)
如果希望记住元素的添加顺序,且增删首尾数据的情况较多? 用LinkedList集合(有序、可重复、有索引),底层基于双链表实现的。
如果不在意元素顺序,也没有重复元素需要存储,只希望增删改查都快? 用HashSet集合(无序,不重复,无索引),底层基于哈希表实现的。(常用)
如果希望记住元素的添加顺序,也没有重复元素需要存储,且希望增删改查都快? 用LinkedHashSet集合(有序,不重复,无索引),底层基于哈希表和双链表。
如果要对元素进行排序,也没有重复元素需要存储?且希望增删改查都快? 用TreeSet集合,基于红黑树实现。
使用迭代器遍历集合时,又同时在删除集合中的数据,程序就会出现并发修改异常的错误。
由于增强for循环遍历集合就是迭代器遍历集合的简化写法,因此,使用增强for循环遍历集合,又在同时删除集合中的数据时,程序也会出现并发修改异常的错误
使用迭代器遍历集合,但用迭代器自己的删除方法删除数据即可。
如果能用for循环遍历时:可以倒着遍历并删除;或者从前往后遍历,但删除元素后做i –操作。
Collection的其他相关知识 前置知识:可变参数
就是一种特殊形参,定义在方法、构造器的形参列表里,格式是:数据类型…参数名称;
接下来,我们编写代码来演示一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class ParamTest { public static void main (String[] args) { test(); test(10 ,20 ,30 ); int [] arr = new int []{10 ,20 ,30 ,40 } test(arr); } public static void test (int ...nums) { System.out.println(nums.length); System.out.println(Arrays.toString(nums)); System.out.println("----------------" ); } }
最后还有一些错误写法,需要让大家写代码时注意一下
特点:可以不传数据给它;可以传一个或者同时传多个数据给它;也可以传一个数组给它。
好处:常常用来灵活的接收数据。
可变参数在方法内部就是一个数组。
一个形参列表中可变参数只能有一个
可变参数必须放在形参列表的最后面
Collections
Collections提供的常用静态方法 方法名称 说明 public static boolean addA11(Collection<? super T> c, T… elements) 给集合批呈添加元素
public static void shuffle(List<?> list) 打乱List集合中的元素顺序
public static void sort(Listlist) 对List集合中的元紊进行升序排序
public static void sort(List list,Comparator<? super T c) 对List集合中元素,按照比较器对象指定的规则进行排序
我们把这些方法用代码来演示一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class CollectionsTest { public static void main (String[] args) { List<String> names = new ArrayList <>(); Collections.addAll(names, "张三" ,"王五" ,"李四" , "张麻子" ); System.out.println(names); Collections.shuffle(names); System.out.println(names); List<Integer> list = new ArrayList <>(); list.add(3 ); list.add(5 ); list.add(2 ); Collections.sort(list); System.out.println(list); } }
上面我们往集合中存储的元素要么是Stirng类型,要么是Integer类型,他们本来就有一种自然顺序所以可以直接排序。但是如果我们往List集合中存储Student对象,这个时候想要对List集合进行排序自定义比较规则的。指定排序规则有两种方式,如下:
排序方式1:让元素实现Comparable接口,重写compareTo方法
比如现在想要往集合中存储Studdent对象,首先需要准备一个Student类,实现Comparable接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class Student implements Comparable <Student>{ private String name; private int age; private double height; @Override public int compareTo (Student o) { return this .age - o.age; } }
然后再使用Collections.sort(list集合)
对List集合排序,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 List<Student> students = new ArrayList <>(); students.add(new Student ("蜘蛛精" ,23 ,169.7 )); students.add(new Student ("紫霞" ,22 ,169.8 )); students.add(new Student ("紫霞" ,22 ,169.8 )); students.add(new Student ("至尊宝" ,26 ,169.5 )); Collections.sort(students); System.out.println(students);
排序方式2:使用调用sort方法是,传递比较器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 Collections.sort(students, new Comparator <Student>(){ @Override public int compare (Student o1, Student o2) { return o1.getAge()-o2.getAge(); } }); System.out.println(students);
综合案例 斗地主案例 我们先分析一下业务需求:
总共有54张牌,每一张牌有花色和点数两个属性、为了排序还可以再加一个序号
点数可以是:“3”,"4","5","6","7","8","9","10","J","Q","K","A","2"
花色可以是:“♣”,"♠","♥","♦"
斗地主时:三个玩家没人手里17张牌,剩余3张牌作为底牌
1 2 3 第一步:为了表示每一张牌有哪些属性,首先应该新建一个扑克牌的类 第二步:启动游戏时,就应该提前准备好54 张牌 第三步:接着再完全洗牌、发牌、捋牌、看牌的业务逻辑
先来完成第一步,定义一个扑克类Card
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 public class Card { private String number; private String color; private int size; public Card () { } public Card (String number, String color, int size) { this .number = number; this .color = color; this .size = size; } public String getNumber () { return number; } public void setNumber (String number) { this .number = number; } public String getColor () { return color; } public void setColor (String color) { this .color = color; } public int getSize () { return size; } public void setSize (int size) { this .size = size; } @Override public String toString () { return color + number ; } }
再完成第二步,定义一个房间类,初始化房间时准备好54张牌
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class Room { private List<Card> allCards = new ArrayList <>(); public Room () { String[] numbers = {"3" ,"4" ,"5" ,"6" ,"7" ,"8" ,"9" ,"10" ,"J" ,"Q" ,"K" ,"A" ,"2" }; String[] colors = {"♠" , "♥" , "♣" , "♦" }; int size = 0 ; for (String number : numbers) { size++; for (String color : colors) { Card c = new Card (number, color, size); allCards.add(c); } } Card c1 = new Card ("" , "🃏" , ++size); Card c2 = new Card ("" , "👲" , ++size); Collections.addAll(allCards, c1, c2); System.out.println("新牌:" + allCards); } }
最后完成第三步,定义一个启动游戏的方法,完成洗牌、发牌、捋牌、看牌的业务逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 public void start () { Collections.shuffle(allCards); System.out.println("洗牌后:" + allCards); List<Card> linHuChong = new ArrayList <>(); List<Card> jiuMoZhi = new ArrayList <>(); List<Card> renYingYing = new ArrayList <>(); for (int i = 0 ; i < allCards.size() - 3 ; i++) { Card c = allCards.get(i); if (i % 3 == 0 ){ linHuChong.add(c); }else if (i % 3 == 1 ){ jiuMoZhi.add(c); }else if (i % 3 == 2 ){ renYingYing.add(c); } } sortCards(linHuChong); sortCards(jiuMoZhi); sortCards(renYingYing); System.out.println("啊冲:" + linHuChong); System.out.println("啊鸠:" + jiuMoZhi); System.out.println("盈盈:" + renYingYing); List<Card> lastThreeCards = allCards.subList(allCards.size() - 3 , allCards.size()); System.out.println("底牌:" + lastThreeCards); jiuMoZhi.addAll(lastThreeCards); sortCards(jiuMoZhi); System.out.println("啊鸠抢到地主后:" + jiuMoZhi); } private void sortCards (List<Card> cards) { Collections.sort(cards, new Comparator <Card>() { @Override public int compare (Card o1, Card o2) { return o2.getSize() - o1.getSize(); } }); }
不要忘记了写测试类了,
1 2 3 4 5 6 7 8 9 public class GameDemo { public static void main (String[] args) { Room m = new Room (); m.start(); } }
Map集合 概述
所谓双列集合,就是说集合中的元素是一对一对的。Map集合中的每一个元素是以key=value
的形式存在的,一个key=value
就称之为一个键值对,而且在Java中有一个类叫Entry类,Entry的对象用来表示键值对对象。
所有的Map集合有如下的特点:键不能重复,值可以重复,每一个键只能找到自己对应的值。
Map集合称为双列集合,格式:{key7=value1 , key2=value2 , key3=value3, ..},一次需要存一对数据做为一个元素.
Map集合的每个元素“key=value”称为一个键值对/键值对对象/一个Entry对象,Map集合也被叫做“键值对集合”
Map集合的所有键是不允许重复的,但值可以重复,键和值是一一对应的,每一个键只能找到自己对应的值
下面我们先写一个Map集合,保存几个键值对,体验一下Map集合的特点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class MapTest1 { public static void main (String[] args) { Map<String, Integer> map = new LinkedHashMap <>(); map.put("手表" , 100 ); map.put("手表" , 220 ); map.put("手机" , 2 ); map.put("Java" , 2 ); map.put(null , null ); System.out.println(map); Map<Integer, String> map1 = new TreeMap <>(); map1.put(23 , "Java" ); map1.put(23 , "MySQL" ); map1.put(19 , "李四" ); map1.put(20 , "王五" ); System.out.println(map1); } }
常用方法
为什么要先学习Map的常用方法? Map是双列集合的祖宗,它的功能是全部双列集合都可以继承过来使用的。
Map的常用方法如下: 方法名称 说明 public v put(K key ,v value) 添加元素 public int size() 获取集合的大小 public void clear() 清空集合 public boolean isEmpty() 判断集合是否为空,为空返回true ,反之 public v get(Object key) 根据键获取对应值 public v remove(Object key) 根据键删除整个元素 public boolean containsKey(Object key) 判断是否包含某个键 public boolean containsValue(Object value) 判断是否包含某个值 public Set keySet() 获取全部键的集合 public collection values() 获取Map集合的全部值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 public class MapTest2 { public static void main (String[] args) { Map<String, Integer> map = new HashMap <>(); map.put("手表" , 100 ); map.put("手表" , 220 ); map.put("手机" , 2 ); map.put("Java" , 2 ); map.put(null , null ); System.out.println(map); System.out.println(map.size()); System.out.println(map.isEmpty()); int v1 = map.get("手表" ); System.out.println(v1); System.out.println(map.get("手机" )); System.out.println(map.get("张三" )); System.out.println(map.remove("手表" )); System.out.println(map); System.out.println(map.containsKey("手表" )); System.out.println(map.containsKey("手机" )); System.out.println(map.containsKey("java" )); System.out.println(map.containsKey("Java" )); System.out.println(map.containsValue(2 )); System.out.println(map.containsValue("2" )); Set<String> keys = map.keySet(); System.out.println(keys); Collection<Integer> values = map.values(); System.out.println(values); Map<String, Integer> map1 = new HashMap <>(); map1.put("java1" , 10 ); map1.put("java2" , 20 ); Map<String, Integer> map2 = new HashMap <>(); map2.put("java3" , 10 ); map2.put("java2" , 222 ); map1.putAll(map2); System.out.println(map1); System.out.println(map2); } }
遍历方式 Map集合的遍历方式
键找值 先获取Map集合全部的键,再通过遍历键来找值
需要用到Map的如下方法: 方法名称 说明 public Set keySet() 获取所有键的集合 public V get(Object key) 根据键获取其对应的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class MapTest1 { public static void main (String[] args) { Map<String, Double> map = new HashMap <>(); map.put("蜘蛛精" , 162.5 ); map.put("蜘蛛精" , 169.8 ); map.put("紫霞" , 165.8 ); map.put("至尊宝" , 169.5 ); map.put("牛魔王" , 183.6 ); System.out.println(map); Set<String> keys = map.keySet(); for (String key : keys) { double value = map.get(key); System.out.println(key + "=====>" + value); } } }
键值对 把“键值对“看成一个整体进行遍历(难度较大)
Map集合是用来存储键值对的,而每一个键值对实际上是一个Entry对象。
这里Map集合的第二种方式,是直接获取每一个Entry对象,把Entry存储扫Set集合中去,再通过Entry对象获取键和值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class MapTest2 { public static void main (String[] args) { Map<String, Double> map = new HashMap <>(); map.put("蜘蛛精" , 169.8 ); map.put("紫霞" , 165.8 ); map.put("至尊宝" , 169.5 ); map.put("牛魔王" , 183.6 ); System.out.println(map); Set<Map.Entry<String, Double>> entries = map.entrySet(); for (Map.Entry<String, Double> entry : entries) { String key = entry.getKey(); double value = entry.getValue(); System.out.println(key + "---->" + value); } } }
Lambda JDK1.8开始之后的新技术(非常的简单)
需要用到Map的如下方法 方法名称 说明 default void forEach(Biconsumer<? super K, ? super V> action) 结合lambda遍历Map集合
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class MapTest3 { public static void main (String[] args) { Map<String, Double> map = new HashMap <>(); map.put("蜘蛛精" , 169.8 ); map.put("紫霞" , 165.8 ); map.put("至尊宝" , 169.5 ); map.put("牛魔王" , 183.6 ); System.out.println(map); map.forEach(new BiConsumer <String, Double>() { @Override public void accept (String k, Double v) { System.out.println(k + "---->" + v); } }); map.forEach(( k, v) -> { System.out.println(k + "---->" + v); }); } }
Map集合案例 学习完Map集合的基本用法之后,接下来我们做一个综合案例,将Map集合运用一下。
先分析需求,再考虑怎么用代码实现
1 2 3 4 5 1. 首先可以将80 个学生选择的景点放到一个集合中去(也就是说,集合中的元素是80 个任意的ABCD元素)2. 准备一个Map集合用来存储景点,以及景点被选择的次数3. 遍历80 个学生选择景点的集合,得到每一个景点,判断Map集合中是否包含该景点 如果不包含,则存储"景点=1" 如果包含,则存获取该景点原先的值,再存储"景点=原来的值+1" ; 此时新值会覆盖旧值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public class MapDemo4 { public static void main (String[] args) { List<String> data = new ArrayList <>(); String[] selects = {"A" , "B" , "C" , "D" }; Random r = new Random (); for (int i = 1 ; i <= 80 ; i++) { int index = r.nextInt(4 ); data.add(selects[index]); } System.out.println(data); Map<String, Integer> result = new HashMap <>(); for (String s : data) { if (result.containsKey(s)){ result.put(s, result.get(s) + 1 ); }else { result.put(s, 1 ); } } System.out.println(result); } }
HashMap
HashMap跟HashSet的底层原理是一模一样的,都是基于哈希表实现的。 实际上:原来学的Set系列集合的底层就是基于Map实现的,只是Set集合中的元素只要键数据,不要值数据而已。1 2 3 public HashSet () {map = new HashMap <>(); }
HashMap集合是一种增删改查数据,性能都较好的集合
但是它是无序,不能重复,没有索引支持的(由键决定特点)
HashMap的键依赖hashCode方法和equals方法保证键的唯一
如果键存储的是自定义类型的对象,可以通过重写hashCode和equals方法,这样可以保证多个对象内容一样时,HashMap集合就能认为是重复的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 HashMap底层数据结构: 哈希表结构 JDK8之前的哈希表 = 数组+链表 JDK8之后的哈希表 = 数组+链表+红黑树 哈希表是一种增删改查数据,性能相对都较好的数据结构 往HashMap集合中键值对数据时,底层步骤如下 第1 步:当你第一次往HashMap集合中存储键值对时,底层会创建一个长度为16 的数组 第2 步:把键然后将键和值封装成一个对象,叫做Entry对象 第3 步:再根据Entry对象的键计算hashCode值(和值无关) 第4 步:利用hashCode值和数组的长度做一个类似求余数的算法,会得到一个索引位置 第5 步:判断这个索引的位置是否为null ,如果为null ,就直接将这个Entry对象存储到这个索引位置 如果不为null ,则还需要进行第6 步的判断 第6 步:继续调用equals方法判断两个对象键是否相同 如果equals返回false ,则以链表的形式往下挂 如果equals方法true ,则认为键重复,此时新的键值对会替换就的键值对。 HashMap底层需要注意这几点: 1. 底层数组默认长度为16 ,如果数组中有超过12 个位置已经存储了元素,则会对数组进行扩容2 倍 数组扩容的加载因子是0.75 ,意思是:16 *0.75 =12 2. 数组的同一个索引位置有多个元素、并且在8 个元素以内(包括8 ),则以链表的形式存储 JDK7版本:链表采用头插法(新元素往链表的头部添加) JDK8版本:链表采用尾插法(新元素我那个链表的尾部添加) 3. 数组的同一个索引位置有多个元素、并且超过了8 个,则以红黑树形式存储
从HashMap底层存储键值对的过程中我们发现:决定键是否重复依赖与两个方法,一个是hashCode方法、一个是equals方法。有两个键计算得到的hashCode值相同,并且两个键使用equals比较为true,就认为键重复。
所以,往Map集合中存储自定义对象作为键,为了保证键的唯一性,我们应该重写hashCode方法和equals方法。
比如有如下案例:往HashMap集合中存储Student对象作为键,学生的家庭住址当做值。要求,当学生对象的姓名和年龄相同时就认为键重复。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 public class Student implements Comparable <Student> { private String name; private int age; private double height; @Override public int compareTo (Student o) { return this .age - o.age; } @Override public boolean equals (Object o) { if (this == o) return true ; if (o == null || getClass() != o.getClass()) return false ; Student student = (Student) o; return age == student.age && Double.compare(student.height, height) == 0 && Objects.equals(name, student.name); } @Override public int hashCode () { return Objects.hash(name, age, height); } public Student () { } public Student (String name, int age, double height) { this .name = name; this .age = age; this .height = height; } @Override public String toString () { return "Student{" + "name='" + name + '\'' + ", age=" + age + ", height=" + height + '}' ; } }
写一个测试类,在测试类中,创建HashMap集合,键是Student类型,值是Stirng类型
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Test1HashMap { public static void main (String[] args) { Map<Student, String> map = new HashMap <>(); map.put(new Student ("蜘蛛精" , 25 , 168.5 ), "盘丝洞" ); map.put(new Student ("蜘蛛精" , 25 , 168.5 ), "水帘洞" ); map.put(new Student ("至尊宝" , 23 , 163.5 ), "水帘洞" ); map.put(new Student ("牛魔王" , 28 , 183.5 ), "牛头山" ); System.out.println(map); } }
上面存储的键,有两个蜘蛛精,但是打印出只会有最后一个。
LinkHashMap 学习完HashMap集合的特点,以及底层原理。接下来我们学习一下LinkedHashMap集合。
LinkedHashMap集合的特点也是由键决定的:有序的、不重复、无索引 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class Test2LinkedHashMap { public static void main (String[] args) { LinkedHashMap<String, Integer> map = new LinkedHashMap <>(); map.put("手表" , 100 ); map.put("手表" , 220 ); map.put("手机" , 2 ); map.put("Java" , 2 ); map.put(null , null ); System.out.println(map); } }
运行上面代码发现,如果是LinedHashMap集合键存储和取出的顺序是一样的
如果是HashMap,键存储和取出的顺序是不一致的
LinkedHashMap的底层原理,和LinkedHashSet底层原理是一样的。底层多个一个双向链表来维护键的存储顺序。
取元素时,先取头节点元素,然后再依次取下一个几点,一直到尾结点。所以是有序的。
TreeMap
特点:不重复、无索引、可排序(按照键的大小默认升序排序,只能对键排序)
原理:TreeMap跟TreeSet集合的底层原理是一样的,都是基于红黑树实现的排序。
TreeMap集合同样也支持两种方式来指定排序规则
让类实现Comparable接口,重写比较规则。 TreeMap集合有一个有参数构造器,支持创建Comparator比较器对象,以便用来指定比较规则。
Map集合下面的另一个子类叫TreeMap。根据我们前面学习其他Map集合的经验,我们应该可以猜出TreeMap有什么特点。
TreeMap集合的特点也是由键决定的,默认按照键的升序排列,键不重复,也是无索引的。
TreeMap集合的底层原理和TreeSet也是一样的,底层都是红黑树实现的。所以可以对键进行排序。
比如往TreeMap集合中存储Student对象作为键,排序方法有两种。直接看代码吧
排序方式1: 写一个Student类,让Student类实现Comparable接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class Student implements Comparable <Student>{ private String name; private int age; private double height; public Student () {} public Student (String name, int age, double height) { this .name=name; this .age=age; this .height=height; } @Override public int compareTo (Student o) { return this .age-o.age; } }
排序方式2: 在创建TreeMap集合时,直接传递Comparator比较器对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class Test3TreeMap { public static void main (String[] args) { Map<Student, String> map = new TreeMap <>(new Comparator <Student>() { @Override public int compare (Student o1, Student o2) { return Double.compare(o1.getHeight(), o2.getHeight()); } }); map.put(new Student ("蜘蛛精" , 25 , 168.5 ), "盘丝洞" ); map.put(new Student ("蜘蛛精" , 25 , 168.5 ), "水帘洞" ); map.put(new Student ("至尊宝" , 23 , 163.5 ), "水帘洞" ); map.put(new Student ("牛魔王" , 28 , 183.5 ), "牛头山" ); System.out.println(map); } }
这种方式都可以对TreeMap集合中的键排序。注意:只有TreeMap的键才能排序,HashMap键不能排序。
补充知识:集合的嵌套
1 2 3 4 5 1. 从需求中我们可以看到,有三个省份,每一个省份有多个城市 我们可以用一个Map集合的键表示省份名称,而值表示省份有哪些城市 2. 而又因为一个身份有多个城市,同一个省份的多个城市可以再用一个List集合来存储。 所以Map集合的键是String类型,而指是List集合类型 HashMap<String, List<String>> map = new HashMap <>();
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public class Test { public static void main (String[] args) { Map<String, List<String>> map = new HashMap <>(); List<String> cities1 = new ArrayList <>(); Collections.addAll(cities1, "南京市" ,"扬州市" ,"苏州市" ,"无锡市" ,"常州市" ); map.put("江苏省" , cities1); List<String> cities2 = new ArrayList <>(); Collections.addAll(cities2, "武汉市" ,"孝感市" ,"十堰市" ,"宜昌市" ,"鄂州市" ); map.put("湖北省" , cities2); List<String> cities3 = new ArrayList <>(); Collections.addAll(cities3, "石家庄市" ,"唐山市" , "邢台市" , "保定市" , "张家口市" ); map.put("河北省" , cities3); System.out.println(map); List<String> cities = map.get("湖北省" ); for (String city : cities) { System.out.println(city); } map.forEach((p, c) -> { System.out.println(p + "----->" + c); }); } }
Stream流 认识Stream
也叫Stream流,是Jdk8开始新增的一套APl(java.util.stream.*),可以用于操作集合或者数组的数据。 用Stream流来做,代码是这样的1 2 List<String> list2 = names.stream().filter(s -> s.startsWith("张" )).filter(a -> a.length()==3 ).collect(Collectors.toList()); System.out.println(list2);
优势: Stream流大量的结合了Lambda的语法风格来编程,提供了一种更加强大,更加简单的方式操作集合或者数组中的数据,代码更简洁,可读性更好。
1 2 3 4 5 主要掌握下面四点: 1 、如何获取List集合的Stream流? 2 、如何获取Set集合的Stream流? 3 、如何获取Map集合的Stream流? 4 、如何获取数组的Stream流?
直接上代码演示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public class StreamTest2 { public static void main (String[] args) { List<String> names = new ArrayList <>(); Collections.addAll(names, "张三丰" ,"张无忌" ,"周芷若" ,"赵敏" ,"张强" ); Stream<String> stream = names.stream(); Set<String> set = new HashSet <>(); Collections.addAll(set, "刘德华" ,"张曼玉" ,"蜘蛛精" ,"马德" ,"德玛西亚" ); Stream<String> stream1 = set.stream(); stream1.filter(s -> s.contains("德" )).forEach(s -> System.out.println(s)); Map<String, Double> map = new HashMap <>(); map.put("古力娜扎" , 172.3 ); map.put("迪丽热巴" , 168.3 ); map.put("马尔扎哈" , 166.3 ); map.put("卡尔扎巴" , 168.3 ); Set<String> keys = map.keySet(); Stream<String> ks = keys.stream(); Collection<Double> values = map.values(); Stream<Double> vs = values.stream(); Set<Map.Entry<String, Double>> entries = map.entrySet(); Stream<Map.Entry<String, Double>> kvs = entries.stream(); kvs.filter(e -> e.getKey().contains("巴" )) .forEach(e -> System.out.println(e.getKey()+ "-->" + e.getValue())); String[] names2 = {"张翠山" , "东方不败" , "唐大山" , "独孤求败" }; Stream<String> s1 = Arrays.stream(names2); Stream<String> s2 = Stream.of(names2); } }
Stream的常用方法
获取Stream流?
Stream类提供的如下方法 说明 public static Stream of(T… values) 获取当前接收数据的Stream流
中间方法指的是:调用完方法之后其结果是一个新的Stream流,于是可以继续调用方法,这样一来就可以支持链式编程 (或者叫流式编程)。
Stream流常见的中间方法 中间方法指的是调用完成后会返回新的Stream流,可以继续使用(支持链式编程)。 Stream提供的常用中间方法 说明 Stream filter(Predicate<? super T> predicate) 用于对流中的数据进行过滤。 Stream sorted() 对元素进行升序排序 Stream sorted(Comparator< ? super I>comparator) 按照指定规则排序 Stream limit( long maxSize) 获取前几个元素 Stream skip( long n) 跳过前几个元素 stream distinct() 去除流中重复的元素。Stream map(Function<? super L,? extends R> mapper)对元素进行加工,并返回对应的新流 static Stream concat(Stream a,stream b) 合并a和b两个流为一个流
代码演示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 public class StreamTest3 { public static void main (String[] args) { List<Double> scores = new ArrayList <>(); Collections.addAll(scores, 88.5 , 100.0 , 60.0 , 99.0 , 9.5 , 99.6 , 25.0 ); scores.stream().filter(s -> s >= 60 ).sorted().forEach(s -> System.out.println(s)); List<Student> students = new ArrayList <>(); Student s1 = new Student ("蜘蛛精" , 26 , 172.5 ); Student s2 = new Student ("蜘蛛精" , 26 , 172.5 ); Student s3 = new Student ("紫霞" , 23 , 167.6 ); Student s4 = new Student ("白晶晶" , 25 , 169.0 ); Student s5 = new Student ("牛魔王" , 35 , 183.3 ); Student s6 = new Student ("牛夫人" , 34 , 168.5 ); Collections.addAll(students, s1, s2, s3, s4, s5, s6); students.stream().filter(s -> s.getAge() >= 23 && s.getAge() <= 30 ) .sorted((o1, o2) -> o2.getAge() - o1.getAge()) .forEach(s -> System.out.println(s)); students.stream().sorted((o1, o2) -> Double.compare(o2.getHeight(), o1.getHeight())) .limit(3 ).forEach(System.out::println); System.out.println("-----------------------------------------------" ); students.stream().sorted((o1, o2) -> Double.compare(o2.getHeight(), o1.getHeight())) .skip(students.size() - 2 ).forEach(System.out::println); students.stream().filter(s -> s.getHeight() > 168 ).map(Student::getName) .distinct().forEach(System.out::println); students.stream().filter(s -> s.getHeight() > 168 ) .distinct().forEach(System.out::println); Stream<String> st1 = Stream.of("张三" , "李四" ); Stream<String> st2 = Stream.of("张三2" , "李四2" , "王五" ); Stream<String> allSt = Stream.concat(st1, st2); allSt.forEach(System.out::println); } }
Stream流常见的终结方法
收集Stream流:就是把Stream流操作后的结果转回到集合或者数组中去返回。
Stream流:方便操作集合/数组的手段;集合/数组:才是开发中的目的。
Stream提供的常用终结方法 说明 R collect(Collector collector) 把流处理后的结果收集到一个指定的集合中去 object[ ] toArray() 把流处理后的结果收集到一个数组中去
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 public class StreamTest4 { public static void main (String[] args) { List<Student> students = new ArrayList <>(); Student s1 = new Student ("蜘蛛精" , 26 , 172.5 ); Student s2 = new Student ("蜘蛛精" , 26 , 172.5 ); Student s3 = new Student ("紫霞" , 23 , 167.6 ); Student s4 = new Student ("白晶晶" , 25 , 169.0 ); Student s5 = new Student ("牛魔王" , 35 , 183.3 ); Student s6 = new Student ("牛夫人" , 34 , 168.5 ); Collections.addAll(students, s1, s2, s3, s4, s5, s6); long size = students.stream().filter(s -> s.getHeight() > 168 ).count(); System.out.println(size); Student s = students.stream().max((o1, o2) -> Double.compare(o1.getHeight(), o2.getHeight())).get(); System.out.println(s); Student ss = students.stream().min((o1, o2) -> Double.compare(o1.getHeight(), o2.getHeight())).get(); System.out.println(ss); List<Student> students1 = students.stream().filter(a -> a.getHeight() > 170 ).collect(Collectors.toList()); System.out.println(students1); Set<Student> students2 = students.stream().filter(a -> a.getHeight() > 170 ).collect(Collectors.toSet()); System.out.println(students2); Map<String, Double> map = students.stream().filter(a -> a.getHeight() > 170 ) .distinct().collect(Collectors.toMap(a -> a.getName(), a -> a.getHeight())); System.out.println(map); Student[] arr = students.stream().filter(a -> a.getHeight() > 170 ).toArray(len -> new Student [len]); System.out.println(Arrays.toString(arr)); } }
File、IO流 File
File是java.io.包下的类,File类的对象,用于代表当前操作系统的文件(可以是文件、或文件夹)。
获取文件信息(大小,文件名,修改时间) 判断文件的类型 创建文件/文件夹 删除文件/文件夹
但是需要我们注意: File对象只能对文件进行操作,不能操作文件中的内容。
创建对象 学习File类和其他类一样,第一步是创建File类的对象。 想要创建对象,我们得看File类有哪些构造方法。
创建File类的对象 构造器 说明 public File(String pathname) 根据文件路径创建文件对象 public File(string parent,String child) 根据父路径和子路径名字创建文件对象 public File(File parent,String child) 根据父路径对应文件对象和子路径名字创建文件对象
下面我们演示一下,File类创建对象的代码
1 需求我们注意的是:路径中"\"要写成" \\", 路径中" /"可以直接用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class FileTest1 { public static void main (String[] args) { File f1 = new File ("D:" + File.separator +"resource" + File.separator + "ab.txt" ); System.out.println(f1.length()); File f2 = new File ("D:/resource" ); System.out.println(f2.length()); File f3 = new File ("D:/resource/aaaa.txt" ); System.out.println(f3.length()); System.out.println(f3.exists()); File f4 = new File ("file-io-app\\src\\itheima.txt" ); System.out.println(f4.length()); } }
File对象既可以代表文件、 也可以代表文件夹。
File封装的对象仅仅是一 个路径名, 这个路径可以是存在的,也允许是不存在的。
常见方法1:判断文件类型、获取文件信息
File提供的判断文件类型、获取文件信息功能 方法名称 说明 public boolean exists() 判断当前文件对象,对应的文件路径是否存在,存在返回true public boolean isFile() 判断当前文件对象指代的是否是文件,是文件返回true,反之。 public boolean isDirectory() 判断当前文件对象指代的是否是文件夹,是文件夹返回true,反之。 public string getName() 获取文件的名称(包含后缀) public long length() 获取文件的大小,返回字节个数 public long lastModified() 获取文件的最后修改时间。 public String getPath() 获取创建文件对象时,使用的路径 public String getAbsolutePath() 获取绝对路径
话不多少,直接上代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class FileTest2 { public static void main (String[] args) throws UnsupportedEncodingException { File f1 = new File ("D:/resource/ab.txt" ); System.out.println(f1.exists()); System.out.println(f1.isFile()); System.out.println(f1.isDirectory()); } }
除了判断功能还有一些获取功能,看代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 File f1 = new File ("D:/resource/ab.txt" );System.out.println(f1.getName()); System.out.println(f1.length()); long time = f1.lastModified();SimpleDateFormat sdf = new SimpleDateFormat ("yyyy/MM/dd HH:mm:ss" );System.out.println(sdf.format(time)); File f2 = new File ("D:\\resource\\ab.txt" );File f3 = new File ("file-io-app\\src\\itheima.txt" );System.out.println(f2.getPath()); System.out.println(f3.getPath()); System.out.println(f2.getAbsolutePath()); System.out.println(f3.getAbsolutePath());
常用方法2:创建文件、删除文件
File类创建文件的功能 方法名称 说明 public boolean createNewFile() 创建一个新的空的文件 public boolean mkdir() 只能创建一级文件夹 public boolean mkdirs() 可以创建多级文件夹
File类删除文件的功能 方法名称 说明 public boolean delete() 删除文件、空文件夹 注意:delete方法默认只能删除文件和空文件夹,删除后的文件不会进入回收站。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class FileTest3 { public static void main (String[] args) throws Exception { File f1 = new File ("D:/resource/itheima2.txt" ); System.out.println(f1.createNewFile()); File f2 = new File ("D:/resource/aaa" ); System.out.println(f2.mkdir()); File f3 = new File ("D:/resource/bbb/ccc/ddd/eee/fff/ggg" ); System.out.println(f3.mkdirs()); System.out.println(f1.delete()); System.out.println(f2.delete()); File f4 = new File ("D:/resource" ); System.out.println(f4.delete()); } }
需要注意的是:
1 2 3 1. mkdir(): 只能创建单级文件夹、2. mkdirs(): 才能创建多级文件夹3. delete(): 文件可以直接删除,但是文件夹只能删除空的文件夹,文件夹有内容删除不了。
常用方法3:遍历文件夹
File类提供的遍历文件夹的功能 方法名称 说明 public string[] list() 获取当前目录下所有的”一级文件名称”到一个字符串数组中去返回。 public File[] listFiles() 获取当前目录下所有的”一级文件对象”到一个文件对象数组中去返回(重点)
演示一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class FileTest4 { public static void main (String[] args) { File f1 = new File ("D:\\course\\待研发内容" ); String[] names = f1.list(); for (String name : names) { System.out.println(name); } File[] files = f1.listFiles(); for (File file : files) { System.out.println(file.getAbsolutePath()); } File f = new File ("D:/resource/aaa" ); File[] files1 = f.listFiles(); System.out.println(Arrays.toString(files1)); } }
这里需要注意几个问题
1 2 3 4 5 1. 当主调是文件时,或者路径不存在时,返回null 2. 当主调是空文件夹时,返回一个长度为0 的数组3. 当主调是一个有内容的文件夹时,将里面所有一级文件和文件夹路径放在File数组中,并把数组返回4. 当主调是一个文件夹,且里面有隐藏文件时,将里面所有文件和文件夹的路径放在FIle数组中,包含隐藏文件5. 当主调是一个文件夹,但是没有权限访问时,返回null
方法递归 递归算法引入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class RecursionTest1 { public static void main (String[] args) { test1(); } public static void test1 () { System.out.println("----test1---" ); test1(); } public static void test2 () { System.out.println("---test2---" ); test3(); } public static void test3 () { test2(); } }
如果直接执行上面的代码,会进入死循环,最终导致栈内存溢出
以上只是用代码演示了一下,递归的形式。在下一节,在通过一个案例来给同学们讲一讲递归的执行流程。
递归算法的执行流程 为了弄清楚递归的执行流程,接下来我们通过一个案例来学习一下。
案例需求:计算n的阶乘,比如5的阶乘 = 1 * 2 * 3 * 4 * 5 ; 6 的阶乘 = 1 * 2 * 3 * 4 * 5 * 6
分析需求用递归该怎么做
1 2 3 4 5 6 7 8 9 10 假设f(n)表示n的阶乘,那么我们可以推导出下面的式子 f(5 ) = 1 +2 +3 +4 +5 f(5 ) = f(4 )+5 f(4 ) = f(3 )+4 f(3 ) = f(2 )+3 f(2 ) = f(1 )+2 f(1 ) = 1 总结规律: 除了f(1 ) = 1 ; 出口 其他的f(n) = f(n-1 )+n
我们可以把f(n)当做一个方法,那么方法的写法如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class RecursionTest2 { public static void main (String[] args) { System.out.println("5的阶乘是:" + f(5 )); } public static int f (int n) { if (n == 1 ){ return 1 ; }else { return f(n - 1 ) * n; } } }
这个代码的执行流程,我们用内存图的形式来分析一下,该案例中递归调用的特点是:一层一层调用,再一层一层往回返。
递归文件搜索 学习完递归算法执行流程后,最后我们回过头来。再来看一下,如果使用递归来遍历文件夹。
案例需求:在D:\\
判断下搜索QQ.exe这个文件,然后直接输出。
1 2 3 4 5 6 1. 先调用文件夹的listFiles方法,获取文件夹的一级内容,得到一个数组2. 然后再遍历数组,获取数组中的File对象3. 因为File对象可能是文件也可能是文件夹,所以接下来就需要判断 判断File对象如果是文件,就获取文件名,如果文件名是`QQ.exe`则打印,否则不打印 判断File对象如果是文件夹,就递归执行1 ,2 ,3 步骤 所以:把1 ,2 ,3 步骤写成方法,递归调用即可。
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 public class RecursionTest3 { public static void main (String[] args) throws Exception { searchFile(new File ("D:/" ) , "QQ.exe" ); } public static void searchFile (File dir, String fileName) throws Exception { if (dir == null || !dir.exists() || dir.isFile()){ return ; } File[] files = dir.listFiles(); if (files != null && files.length > 0 ){ for (File f : files) { if (f.isFile()){ if (f.getName().contains(fileName)){ System.out.println("找到了:" + f.getAbsolutePath()); Runtime runtime = Runtime.getRuntime(); runtime.exec(f.getAbsolutePath()); } }else { searchFile(f, fileName); } } } } }
字符集 字符集的来历 所以,接下来我们正式学习一下字符集。先来带着同学们,了解一下字符集的来历。
我们知道计算机是美国人发明的,由于计算机能够处理的数据只能是0和1组成的二进制数据,为了让计算机能够处理字符,于是美国人就把他们会用到的每一个字符进行了编码(所谓编码,就是为一个字符编一个二进制数据 ),如下图所示:
美国人常用的字符有英文字母、标点符号、数字以及一些特殊字符,这些字符一共也不到128个,所以他们用1个字节来存储1字符就够了。 美国人把他们用到的字符和字符对应的编码总结成了一张码表,这张码表叫做ASCII码表(也叫ASCII字符集)。
其实计算机只在美国用是没有问题的,但是计算机慢慢的普及到全世界,当普及到中国的时候,在计算机中想要存储中文,那ASCII字符集就不够用了,因为中文太多了,随便数一数也有几万个字符。
于是中国人为了在计算机中存储中文,也编了一个中国人用的字符集叫做GBK字符集,这里面包含2万多个汉字字符,GBK中一个汉字采用两个字节来存储 ,为了能够显示英文字母,GBK字符集也兼容了ASCII字符集,在GBK字符集中一个字母还是采用一个字节来存储 。
汉字和字母的编码特点 接下来,就带着同学们了解一下,计算机是怎么识别中文和英文的。
比如:在文件中存储一个我a你
,底层其实存储的是这样的二进制数据。
需要我们注意汉字和字母的编码特点:
如果是存储字母,采用1个字节来存储,一共8位,其中第1位是0
如果是存储汉字,采用2个字节来存储,一共16位,其中第1位是1
当读取文件中的字符时,通过识别读取到的第1位是0还是1来判断是字母还是汉字
如果读取到第1位是0,就认为是一个字母,此时往后读1个字节。
如果读取到第1位是1,就认为是一个汉字,此时往后读2个字节。
Unicode字符集 同学们注意了,咱们国家可以用GBK字符集来表示中国人使用的文字,那世界上还有很多其他的国家,他们也有自己的文字,他们也想要自己国家的文字在计算机中处理,于是其他国家也在搞自己的字符集,就这样全世界搞了上百个字符集,而且各个国家的字符集互不兼容。 这样其实很不利于国际化的交流,可能一个文件在我们国家的电脑上打开好好的,但是在其他国家打开就是乱码了。
为了解决各个国家字符集互不兼容的问题,由国际化标准组织牵头,设计了一套全世界通用的字符集,叫做Unicode字符集。在Unicode字符集中包含了世界上所有国家的文字,一个字符采用4个自己才存储。
在Unicode字符集中,采用一个字符4个字节的编码方案,又造成另一个问题:如果是说英语的国家,他们只需要用到26大小写字母,加上一些标点符号就够了,本身一个字节就可以表示完,用4个字节就有点浪费。
于是又对Unicode字符集中的字符进行了重新编码,一共设计了三种编码方案。分别是UTF-32、UTF-16、UTF-8; 其中比较常用的编码方案是UTF-8
下面我们详细介绍一下UTF-8这种编码方案的特点。
1 2 3 4 1. UTF-8 是一种可变长的编码方案,工分为4 个长度区2. 英文字母、数字占1 个字节兼容(ASCII编码)3. 汉字字符占3 个字节4. 极少数字符占4 个字节
字符集小结 最后,我们将前面介绍过的字符集小结一下
1 2 3 4 5 6 7 8 ASCII字符集:《美国信息交换标准代码》,包含英文字母、数字、标点符号、控制字符 特点:1 个字符占1 个字节 GBK字符集:中国人自己的字符集,兼容ASCII字符集,还包含2 万多个汉字 特点:1 个字母占用1 个字节;1 个汉字占用2 个字节 Unicode字符集:包含世界上所有国家的文字,有三种编码方案,最常用的是UTF-8 UTF-8 编码方案:英文字母、数字占1 个字节兼容(ASCII编码)、汉字字符占3 个字节
编码和解码 搞清楚字符集的知识之后,我们接下来再带着同学们使用Java代码完成编码和解码的操作。
其实String类类中就提供了相应的方法,可以完成编码和解码的操作。
编码:把字符串按照指定的字符集转换为字节数组
解码:把字节数组按照指定的字符集转换为字符串
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class Test { public static void main (String[] args) throws Exception { String data = "a我b" ; byte [] bytes = data.getBytes(); System.out.println(Arrays.toString(bytes)); byte [] bytes1 = data.getBytes("GBK" ); System.out.println(Arrays.toString(bytes1)); String s1 = new String (bytes); System.out.println(s1); String s2 = new String (bytes1, "GBK" ); System.out.println(s2); } }
IO流(字节流) IO流概述 各位小伙伴,在前面我们已经学习过File类。但是我们知道File只能操作文件,但是不能操作文件中的内容。我们也学习了字符集,不同的字符集存字符数据的原理是不一样的。有了前面两个知识的基础,接下来我们再学习IO流,就可以对文件中的数据进行操作了。
IO流的作用:就是可以对文件或者网络中的数据进行读、写的操作。如下图所示
把数据从磁盘、网络中读取到程序中来,用到的是输入流。
把程序中的数据写入磁盘、网络中,用到的是输出流。
简单记:输入流(读数据)、输出流(写数据)
IO流在Java中有很多种,不同的流来干不同的事情。Java把各种流用不同的类来表示,这些流的继承体系如下图所示:
1 2 3 IO流分为两大派系: 1. 字节流:字节流又分为字节输入流、字节输出流 2. 字符流:字符流由分为字符输入流、字符输出流
同学们,在上节课认识了什么是IO流,接下来我们学习字节流中的字节输入流,用InputStream来表示。但是InputStream是抽象类,我们用的是它的子类,叫FileInputStream。
需要用到的方法如下图所示:有构造方法、成员方法
构造器 说明 public FileInputStream(File file) 创建字节输入流管道与源文件接通 public FileInputStream(String pathname) 创建字节输入流管道与源文件接通
方法名称 说明 public int read() 每次读取一个字节返回,如果发现没有数据可读会返回-1. public int read(byte[] buffer) 每次用一个字节数组去读取数据,返回字节数组读取了多少个字节,如果发现没有数据可读会返回-1.
使用FileInputStream读取文件中的字节数据,步骤如下
1 2 3 第一步:创建FileInputStream文件字节输入流管道,与源文件接通。 第二步:调用read()方法开始读取文件的字节数据。 第三步:调用close()方法释放资源
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class FileInputStreamTest1 { public static void main (String[] args) throws Exception { InputStream is = new FileInputStream (("file-io-app\\src\\itheima01.txt" )); int b; while ((b = is.read()) != -1 ){ System.out.print((char ) b); } is.close(); } }
这里需要注意一个问题:由于一个中文在UTF-8编码方案中是占3个字节,采用一次读取一个字节的方式,读一个字节就相当于读了1/3个汉字,此时将这个字节转换为字符,是会有乱码的。
我们学习了FileInputStream调用read()方法,可以一次读取一个字节。但是这种读取方式效率太太太太慢了。 为了提高效率,我们可以使用另一个read(byte[] bytes)的重载方法,可以一次读取多个字节,至于一次读多少个字节,就在于你传递的数组有多大。
使用FileInputStream一次读取多个字节的步骤如下
1 2 3 第一步:创建FileInputStream文件字节输入流管道,与源文件接通。 第二步:调用read(byte [] bytes)方法开始读取文件的字节数据。 第三步:调用close()方法释放资源
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class FileInputStreamTest2 { public static void main (String[] args) throws Exception { InputStream is = new FileInputStream ("file-io-app\\src\\itheima02.txt" ); byte [] buffer = new byte [3 ]; int len; while ((len = is.read(buffer)) != -1 ){ String rs = new String (buffer, 0 , len); System.out.print(rs); } is.close(); } }
需要我们注意的是:read(byte[] bytes)它的返回值,表示当前这一次读取的字节个数。
假设有一个a.txt文件如下:
每次读取过程如下
1 2 3 4 也就是说,并不是每次读取的时候都把数组装满,比如数组是 byte [] bytes = new byte [3 ]; 第一次调用read(bytes)读取了3 个字节(分别是97 ,98 ,99 ),并且往数组中存,此时返回值就是3 第二次调用read(bytes)读取了2 个字节(分别是99 ,100 ),并且往数组中存,此时返回值是2 第三次调用read(bytes)文件中后面已经没有数据了,此时返回值为-1
还需要注意一个问题:采用一次读取多个字节的方式,也是可能有乱码的。因为也有可能读取到半个汉字的情况。
同学们,前面我们到的读取方式,不管是一次读取一个字节,还是一次读取多个字节,都有可能有乱码。那么接下来我们介绍一种,不出现乱码的读取方式。
我们可以一次性读取文件中的全部字节,然后把全部字节转换为一个字符串,就不会有乱码了。
方式一:自己定义一个字节数组与被读取的文件大小一样大,然后使用该字节数组,一次读完文件的全部字节。 方法名称 说明 public int read(byte[] buffer) 每次用一个字节数组去读取,返回字节数组读取了多少个字节,如果发现没有数据可读会返回-1.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 InputStream is = new FileInputStream ("file-io-app\\src\\itheima03.txt" );File f = new File ("file-io-app\\src\\itheima03.txt" );long size = f.length();byte [] buffer = new byte [(int ) size];int len = is.read(buffer);System.out.println(new String (buffer)); is.close();
方式二:Java官方为InputStream提供了如下方法,可以直接把文件的全部字节读取到一个字节数组中返回。 方法名称 说明 public byte[] readAllBytes() throws IOException 直接将当前字节输入流对应的文件对象的字节数据装到一个字节数组返回
1 2 3 4 5 6 7 8 9 10 InputStream is = new FileInputStream ("file-io-app\\src\\itheima03.txt" );byte [] buffer = is.readAllBytes();System.out.println(new String (buffer)); is.close();
最后,还是要注意一个问题:一次读取所有字节虽然可以解决乱码问题,但是文件不能过大,如果文件过大,可能导致内存溢出。
FileOutputStream写字节 使用FIleInputStream读取文件中的字节数据。然后有同学就迫不及待的想学习往文件中写入数据了。
往文件中写数据需要用到OutputStream下面的一个子类FileOutputStream。写输入的流程如下图所示
使用FileOutputStream往文件中写数据的步骤如下:
1 2 3 第一步:创建FileOutputStream文件字节输出流管道,与目标文件接通。 第二步:调用wirte()方法往文件中写数据 第三步:调用close()方法释放资源
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class FileOutputStreamTest4 { public static void main (String[] args) throws Exception { OutputStream os = new FileOutputStream ("file-io-app/src/itheima04out.txt" , true ); os.write(97 ); os.write('b' ); byte [] bytes = "我爱你中国abc" .getBytes(); os.write(bytes); os.write(bytes, 0 , 15 ); os.write("\r\n" .getBytes()); os.close(); } }
字节流复制文件 在前面已经把字节输入流和字节输出流都学习完了。现在我们就可以用这两种流配合起来使用,做一个文件复制的综合案例。
比如:我们要复制一张图片,从磁盘D:/resource/meinv.png
的一个位置,复制到C:/data/meinv.png
位置。
复制文件的思路如下图所示:
1 2 3 1. 需要创建一个FileInputStream流与源文件接通,创建FileOutputStream与目标文件接通2. 然后创建一个数组,使用FileInputStream每次读取一个字节数组的数据,存如数组中3. 然后再使用FileOutputStream把字节数组中的有效元素,写入到目标文件中
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class CopyTest5 { public static void main (String[] args) throws Exception { InputStream is = new FileInputStream ("D:/resource/meinv.png" ); OutputStream os = new FileOutputStream ("C:/data/meinv.png" ); System.out.println(10 / 0 ); byte [] buffer = new byte [1024 ]; int len; while ((len = is.read(buffer)) != -1 ){ os.write(buffer, 0 , len); } os.close(); is.close(); System.out.println("复制完成!!" ); } }
IO流资源释放 JDK7以前的资源释放 在JDK7版本以前,我们可以使用try…catch…finally语句来处理。格式如下
1 2 3 4 5 6 7 8 try { }catch (异常类 e){ }finally { }
改造上面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public class Test2 { public static void main (String[] args) { InputStream is = null ; OutputStream os = null ; try { System.out.println(10 / 0 ); is = new FileInputStream ("file-io-app\\src\\itheima03.txt" ); os = new FileOutputStream ("file-io-app\\src\\itheima03copy.txt" ); System.out.println(10 / 0 ); byte [] buffer = new byte [1024 ]; int len; while ((len = is.read(buffer)) != -1 ){ os.write(buffer, 0 , len); } System.out.println("复制完成!!" ); } catch (IOException e) { e.printStackTrace(); } finally { try { if (os != null ) os.close(); } catch (IOException e) { e.printStackTrace(); } try { if (is != null ) is.close(); } catch (IOException e) { e.printStackTrace(); } } } }
代码写到这里,有很多同学就已经看不下去了。是的,我也看不下去,本来几行代码就写完了的,加上try…catch…finally之后代码多了十几行,而且阅读性并不高。难受….
JDK7以后的资源释放 刚才已经发现了try…catch…finally处理异常,并释放资源代码比较繁琐。Java在JDK7版本为我们提供了一种简化的是否资源的操作,它会自动是否资源。代码写起来也想当简单。
格式如下:
1 2 3 4 5 6 7 try (资源对象1 ; 资源对象2 ;){ 使用资源的代码 }catch (异常类 e){ 处理异常的代码 }
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class Test3 { public static void main (String[] args) { try ( InputStream is = new FileInputStream ("D:/resource/meinv.png" ); OutputStream os = new FileOutputStream ("C:/data/meinv.png" ); ){ byte [] buffer = new byte [1024 ]; int len; while ((len = is.read(buffer)) != -1 ){ os.write(buffer, 0 , len); } System.out.println(conn); System.out.println("复制完成!!" ); } catch (Exception e) { e.printStackTrace(); } } }
字符流 同学们,前面我们学习了字节流,使用字节流可以读取文件中的字节数据。但是如果文件中有中文,使用字节流来读取,就有可能读到半个汉字的情况,这样会导致乱码。虽然使用读取全部字节的方法不会出现乱码,但是如果文件过大又不太合适。
所以Java专门为我们提供了另外一种流,叫字符流,可以字符流是专门为读取文本数据而生的。
FileReader类 先类学习字符流中的FileReader类,这是字符输入流,用来将文件中的字符数据读取到程序中来。
FileReader读取文件的步骤如下:
1 2 3 第一步:创建FileReader对象与要读取的源文件接通 第二步:调用read()方法读取文件中的字符 第三步:调用close()方法关闭流
需要用到的方法:先通过构造器创建对象,再通过read方法读取数据(注意:两个read方法的返回值,含义不一样 )
构造器 说明 public FileReader(File file) 创建字符输入流管道与源文件接通 public FileReader(String pathname) 创建字符输入流管道与源文件接通
方法名称 说明 public int read() 每次读取一个字符返回,如果发现没有数据可读会返回-1 public int read(char[ ] buffer) 如果发现没有数据可读会返回-1.每次用一个字符数组去读取数据,返回字符数组读取了多少个字符,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class FileReaderTest1 { public static void main (String[] args) { try ( Reader fr = new FileReader ("io-app2\\src\\itheima01.txt" ); ){ char [] buffer = new char [3 ]; int len; while ((len = fr.read(buffer)) != -1 ){ System.out.print(new String (buffer, 0 , len)); } } catch (Exception e) { e.printStackTrace(); } } }
FileWriter类 我们学习了FileReader,它可以将文件中的字符数据读取到程序中来。接下来,我们就要学习FileWriter了,它可以将程序中的字符数据写入文件。
FileWriter往文件中写字符数据的步骤如下:
1 2 3 第一步:创建FileWirter对象与要读取的目标文件接通 第二步:调用write(字符数据/字符数组/字符串)方法读取文件中的字符 第三步:调用close()方法关闭流
需要用到的方法如下:构造器是用来创建FileWriter对象的,有了对象才能调用write方法写数据到文件。
构造器 说明 public Filewriter(File file) 创建字节输出流管道与源文件对象接通
public Filewriter(string filepath) 创建字节输出流管道与源文件路径接通
public Filewriter(File file,boolean append) 创建字节输出流管道与源文件对象接通,可追加数据
public Filewriter(string filepath, boolean append) 创建字节输出流管道与源文件路径接通,可追加数据
方法名称 说明 void write(int c) 写一个字符 void write(String str) 写一个字符串 void write(String str, int off, int len) 写一个字符串的一部分 void write(char[]cbuf) 写入一个字符数组 void write(char[] cbuf, int off, jint len) 写入字符数组的一部分
接下来,用代码演示一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public class FileWriterTest2 { public static void main (String[] args) { try ( Writer fw = new FileWriter ("io-app2/src/itheima02out.txt" , true ); ){ fw.write('a' ); fw.write(97 ); fw.write("\r\n" ); fw.write("我爱你中国abc" ); fw.write("\r\n" ); fw.write("我爱你中国abc" , 0 , 5 ); fw.write("\r\n" ); char [] buffer = {'黑' , '马' , 'a' , 'b' , 'c' }; fw.write(buffer); fw.write("\r\n" ); fw.write(buffer, 0 , 2 ); fw.write("\r\n" ); } catch (Exception e) { e.printStackTrace(); } } }
FileWriter写的注意事项 各位同学,刚才我们已经学习了FileWriter字符输出流的基本使用。但是,这里有一个小问题需要和同学们说下一:FileWriter写完数据之后,必须刷新或者关闭,写出去的数据才能生效。
比如:下面的代码只调用了写数据的方法,没有关流的方法。当你打开目标文件时,是看不到任何数据的。
1 2 3 4 5 6 7 Writer fw = new FileWriter ("io-app2/src/itheima03out.txt" );fw.write('a' ); fw.write('b' ); fw.write('c' );
而下面的代码,加上了flush()方法之后,数据就会立即到目标文件中去。
1 2 3 4 5 6 7 8 9 10 Writer fw = new FileWriter ("io-app2/src/itheima03out.txt" );fw.write('a' ); fw.write('b' ); fw.write('c' ); fw.flush();
下面的代码,调用了close()方法,数据也会立即到文件中去。因为close()方法在关闭流之前,会将内存中缓存的数据先刷新到文件,再关流。
1 2 3 4 5 6 7 8 9 10 Writer fw = new FileWriter ("io-app2/src/itheima03out.txt" );fw.write('a' ); fw.write('b' ); fw.write('c' ); fw.close();
但是需要注意的是,关闭流之后,就不能在对流进行操作了。否则会出异常
缓冲流 学习完字符流之后,接下来我们学习一下缓冲流。我们还是先来认识一下缓存流,再来说一下它的作用。缓冲流有四种,如下图所示
缓冲流的作用 :可以对原始流进行包装,提高原始流读写数据的性能。
缓冲字节流 我们先来学习字节缓冲流是如何提高读写数据的性能的,原理如下图所示。是因为在缓冲流的底层自己封装了一个长度为8KB(8129byte)的字节数组,但是缓冲流不能单独使用,它需要依赖于原始流。
读数据时: 它先用原始字节输入流一次性读取8KB的数据存入缓冲流内部的数组中(ps: 先一次多囤点货),再从8KB的字节数组中读取一个字节或者多个字节(把消耗屯的货)。
写数据时: 它是先把数据写到缓冲流内部的8BK的数组中(ps: 先攒一车货),等数组存满了,再通过原始的字节输出流,一次性写到目标文件中去(把囤好的货,一次性运走)。
在创建缓冲字节流对象时,需要封装一个原始流对象进来。构造方法如下
构造器 说明 public BufferedInputStream(InputStream is) 把低级的字节输入流包装成一个高级的缓冲字节输入流,从而提高读数据的性能
public BufferedOutputStream(OutputStream os) 把低级的字节输出流包装成一个高级的缓冲字节输出流,从而提高写数据的性能
如果我们用缓冲流复制文件,代码写法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class BufferedInputStreamTest1 { public static void main (String[] args) { try ( InputStream is = new FileInputStream ("io-app2/src/itheima01.txt" ); InputStream bis = new BufferedInputStream (is); OutputStream os = new FileOutputStream ("io-app2/src/itheima01_bak.txt" ); OutputStream bos = new BufferedOutputStream (os); ){ byte [] buffer = new byte [1024 ]; int len; while ((len = bis.read(buffer)) != -1 ){ bos.write(buffer, 0 , len); } System.out.println("复制完成!!" ); } catch (Exception e) { e.printStackTrace(); } } }
2.2 字符缓冲流 接下来,我们学习另外两个缓冲流——字符缓冲流。它的原理和字节缓冲流是类似的,它底层也会有一个8KB的数组,但是这里是字符数组。字符缓冲流也不能单独使用,它需要依赖于原始字符流一起使用。
BufferedReader读数据时: 它先原始字符输入流一次性读取8KB的数据存入缓冲流内部的数组中(ps: 先一次多囤点货),再从8KB的字符数组中读取一个字符或者多个字符(把消耗屯的货)。
创建BufferedReader对象需要用到BufferedReader的构造方法,内部需要封装一个原始的字符输入流,我们可以传入FileReader.
构造器 说明 public BufferedReader(Reader n) 把低级的字符输入流包装成字符缓冲输入流管道,从而提高字符输入流读字符数据的性能
而且BufferedReader还要特有的方法,一次可以读取文本文件中的一行
方法 说明 public string readLine() 读取一行数据返回,如果没有数据可读了,会返回null
使用BufferedReader读取数据的代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class BufferedReaderTest2 { public static void main (String[] args) { try ( Reader fr = new FileReader ("io-app2\\src\\itheima04.txt" ); BufferedReader br = new BufferedReader (fr); ){ String line; while ((line = br.readLine()) != null ){ System.out.println(line); } } catch (Exception e) { e.printStackTrace(); } } }
BufferedWriter写数据时: 它是先把数据写到字符缓冲流内部的8BK的数组中(ps: 先攒一车货),等数组存满了,再通过原始的字符输出流,一次性写到目标文件中去(把囤好的货,一次性运走)。如下图所示
创建BufferedWriter对象时需要用到BufferedWriter的构造方法,而且内部需要封装一个原始的字符输出流,我们这里可以传递FileWriter。
构造器 说明 public BufferedWriter(Writer r) 把低级的字符输出流包装成一个高级的缓冲字符输出流管道,从而提高字符输出流写数据的性能
而且BufferedWriter新增了一个功能,可以用来写一个换行符
方法 说明 public void newLine() 换行
接下来,用代码演示一下,使用BufferedWriter往文件中写入字符数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class BufferedWriterTest3 { public static void main (String[] args) { try ( Writer fw = new FileWriter ("io-app2/src/itheima05out.txt" , true ); BufferedWriter bw = new BufferedWriter (fw); ){ bw.write('a' ); bw.write(97 ); bw.write('磊' ); bw.newLine(); bw.write("我爱你中国abc" ); bw.newLine(); } catch (Exception e) { e.printStackTrace(); } } }
缓冲流性能分析 我们说缓冲流内部多了一个数组,可以提高原始流的读写性能。讲到这一定有同学有这么一个疑问,它和我们使用原始流,自己加一个8BK数组不是一样的吗? 缓冲流就一定能提高性能吗?先告诉同学们答案,缓冲流不一定能提高性能 。
下面我们用一个比较大文件(889MB)复制,做性能测试,分别使用下面四种方式来完成文件复制,并记录文件复制的时间。
① 使用低级流一个字节一个字节的复制
② 使用低级流按照字节数组的形式复制
③ 使用缓冲流一个字节一个字节的复制
④ 使用缓冲流按照字节数组的形式复制
1 2 3 4 5 低级流一个字节复制: 慢得简直让人无法忍受 低级流按照字节数组复制(数组长度1024 ): 12. 117s 缓冲流一个字节复制: 11. 058s 缓冲流按照字节数组复制(数组长度1024 ): 2. 163s 【注意:这里的测试只能做一个参考,和电脑性能也有直接关系】
经过上面的测试,我们可以得出一个结论:默认情况下,采用一次复制1024个字节,缓冲流完胜。
但是,缓冲流就一定性能高吗?我们采用一次复制8192个字节试试
1 2 低级流按照字节数组复制(数组长度8192 ): 2. 535s 缓冲流按照字节数组复制(数组长度8192 ): 2. 088s
经过上面的测试,我们可以得出一个结论:一次读取8192个字节时,低级流和缓冲流性能相当。 相差的那几毫秒可以忽略不计。
继续把数组变大,看一看缓冲流就一定性能高吗?现在采用一次读取1024*32个字节数据试试
1 2 低级流按照字节数组复制(数组长度8192 ): 1. 128s 缓冲流按照字节数组复制(数组长度8192 ): 1. 133s
经过上面的测试,我们可以得出一个结论:数组越大性能越高,低级流和缓冲流性能相当。 相差的那几秒可以忽略不计。
继续把数组变大,看一看缓冲流就一定性能高吗?现在采用一次读取1024*6个字节数据试试
1 2 低级流按照字节数组复制(数组长度8192 ): 1. 039s 缓冲流按照字节数组复制(数组长度8192 ): 1. 151s
此时你会发现,当数组大到一定程度,性能已经提高了多少了,甚至缓冲流的性能还没有低级流高。
最终总结一下:缓冲流的性能不一定比低级流高,其实低级流自己加一个数组,性能其实是不差。 只不过缓冲流帮你加了一个相对而言大小比较合理的数组 。
三、转换流 前面我们学习过FileReader读取文件中的字符,但是同学们注意了,FileReader默认只能读取UTF-8编码格式的文件。如果使用FileReader读取GBK格式的文件,可能存在乱码,因为FileReader它遇到汉字默认是按照3个字节来读取的,而GBK格式的文件一个汉字是占2个字节,这样就会导致乱码。
Java给我们提供了另外两种流InputStreamReader,OutputStreamWriter,这两个流我们把它叫做转换流。它们可以将字节流转换为字符流,并且可以指定编码方案。
接下来,我们先学习InputStreamReader类,你看这个类名就比较有意思,前面是InputStream表示字节输入流,后面是Reader表示字符输入流,合在一起意思就是表示可以把InputStream转换为Reader,最终InputStreamReader其实也是Reader的子类,所以也算是字符输入流。
InputStreamReader也是不能单独使用的,它内部需要封装一个InputStream的子类对象,再指定一个编码表,如果不指定编码表,默认会按照UTF-8形式进行转换。
需求:我们可以先准备一个GBK格式的文件,然后使用下面的代码进行读取,看是是否有乱码。
1床前明月光c 2疑是地上霜b 3低头思姑娘a abcde GBK格式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class InputStreamReaderTest2 { public static void main (String[] args) { try ( InputStream is = new FileInputStream ("io-app2/src/itheima06.txt" ); Reader isr = new InputStreamReader (is, "GBK" ); BufferedReader br = new BufferedReader (isr); ){ String line; while ((line = br.readLine()) != null ){ System.out.println(line); } } catch (Exception e) { e.printStackTrace(); } } }
执行完之后,你会发现没有乱码。
OutputStreamWriter类 接下来,我们先学习OutputStreamWriter类,你看这个类名也比较有意思,前面是OutputStream表示字节输出流,后面是Writer表示字符输出流,合在一起意思就是表示可以把OutputStream转换为Writer,最终OutputStreamWriter其实也是Writer的子类,所以也算是字符输出流。
OutputStreamReader也是不能单独使用的,它内部需要封装一个OutputStream的子类对象,再指定一个编码表,如果不指定编码表,默认会按照UTF-8形式进行转换。
需求:我们可以先准备一个GBK格式的文件,使用下面代码往文件中写字符数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class OutputStreamWriterTest3 { public static void main (String[] args) { try ( OutputStream os = new FileOutputStream ("io-app2/src/itheima07out.txt" ); Writer osw = new OutputStreamWriter (os, "GBK" ); BufferedWriter bw = new BufferedWriter (osw); ){ bw.write("我是中国人abc" ); bw.write("我爱你中国123" ); } catch (Exception e) { e.printStackTrace(); } } }
打印流 接下来,我们学习打印流,其实打印流我们从开学第一天就一直再使用,只是没有学到你感受不到而已。打印流可以实现更加方便,更加高效的写数据的方式。
打印流基本使用 打印流,这里所说的打印其实就是写数据的意思,它和普通的write方法写数据还不太一样,一般会使用打印流特有的方法叫print(数据)
或者println(数据)
,它打印啥就输出啥。
打印流有两个,一个是字节打印流PrintStream,一个是字符打印流PrintWriter,如下图所示
PrintStream和PrintWriter的用法是一样的,所以这里就一块演示了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class PrintTest1 { public static void main (String[] args) { try ( PrintWriter ps = new PrintWriter (new FileOutputStream ("io-app2/src/itheima08.txt" , true )); ){ ps.print(97 ); ps.print('a' ); ps.println("我爱你中国abc" ); ps.println(true ); ps.println(99.5 ); ps.write(97 ); } catch (Exception e) { e.printStackTrace(); } } }
重定向输出语句 其实我们开学第一课,就给同学们讲过System.out.println()
这句话表示打印输出,但是至于为什么能够输出,其实我们一直不清楚。
以前是因为知识储备还不够,无法解释,到现在就可以给同学们揭晓谜底了,因为System里面有一个静态变量叫out,out的数据类型就是PrintStream,它就是一个打印流,而且这个打印流的默认输出目的地是控制台,所以我们调用System.out.pirnln()
就可以往控制台打印输出任意类型的数据,而且打印啥就输出啥。
而且System还提供了一个方法,可以修改底层的打印流,这样我们就可以重定向打印语句的输出目的地了。我们玩一下, 直接上代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class PrintTest2 { public static void main (String[] args) { System.out.println("老骥伏枥" ); System.out.println("志在千里" ); try ( PrintStream ps = new PrintStream ("io-app2/src/itheima09.txt" ); ){ System.setOut(ps); System.out.println("烈士暮年" ); System.out.println("壮心不已" ); } catch (Exception e) { e.printStackTrace(); } } }
此时打印语句,将往文件中打印数据,而不在控制台。
数据流 同学们,接下我们再学习一种流,这种流在开发中偶尔也会用到。比如,我们想把数据和数据的类型一并写到文件中去,读取的时候也将数据和数据类型一并读出来。这就可以用到数据流,有两个DataInputStream和DataOutputStream.
DataOutputStream类 我们先学习DataOutputStream类,它也是一种包装流,创建DataOutputStream对象时,底层需要依赖于一个原始的OutputStream流对象。然后调用它的wirteXxx方法,写的是特定类型的数据。
构造器 说明 public DataOutputstream(outputstream out) 创建新数据输出流包装基础的字节输出流
方法 说明 public final void writeByte(int v) throws IOException 将byte类型的数据写入基础的字节输出流 public final void writeInt(int v) throws IOException 将int类型的数据写入基础的字节输出流 public final void writeDouble(Double v) throws IOException 将double类型的数据写入基础的字节输出流 public final void writeUTF(String str) throws IOException 将字符串数据以UTF-8编码成字节写入基础的字节输出流 public void write(int/byte[ ]/byte[]一部分) 支持写字节数据出去
代码如下:往文件中写整数、小数、布尔类型数据、字符串数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class DataOutputStreamTest1 { public static void main (String[] args) { try ( DataOutputStream dos = new DataOutputStream (new FileOutputStream ("io-app2/src/itheima10out.txt" )); ){ dos.writeInt(97 ); dos.writeDouble(99.5 ); dos.writeBoolean(true ); dos.writeUTF("黑马程序员666!" ); } catch (Exception e) { e.printStackTrace(); } } }
学习完DataOutputStream后,再学习DataIntputStream类,它也是一种包装流,创建DataInputStream对象时,底层需要依赖于一个原始的InputStream流对象。然后调用它的readXxx()方法就可以读取特定类型的数据。
构造器 说明 public DataInputstream(InputStream is) 创建新数据输入流包装基础的字节输入流
方法 说明 Public final byte readByte() throws IOException 读取字节数据返回 public final int readInt() throwsIOException 读取int类型的数据返回 public final double readDouble() throws IOException 读取double类型的数据返回 public final string readUTF() throws IOException 读取字符串数(UTF-8)据返回 public int readInt( )/read(byte[]) 支持读字节数据进来
代码如下:读取文件中特定类型的数据(整数、小数、字符串等)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class DataInputStreamTest2 { public static void main (String[] args) { try ( DataInputStream dis = new DataInputStream (new FileInputStream ("io-app2/src/itheima10out.txt" )); ){ int i = dis.readInt(); System.out.println(i); double d = dis.readDouble(); System.out.println(d); boolean b = dis.readBoolean(); System.out.println(b); String rs = dis.readUTF(); System.out.println(rs); } catch (Exception e) { e.printStackTrace(); } } }
序列化流 各位同学同学,还有最后一个流要学习,叫做序列化流。序列化流是干什么用的呢? 我们知道字节流是以字节为单位来读写数据、字符流是按照字符为单位来读写数据、而对象流是以对象为单位来读写数据。也就是把对象当做一个整体,可以写一个对象到文件,也可以从文件中把对象读取出来。
这里有一个新词 序列化,第一次听同学们可能还比较陌生,我来给同学们解释一下
1 2 序列化:意思就是把对象写到文件或者网络中去。(简单记:写对象) 反序列化:意思就是把对象从文件或者网络中读取出来。(简单记:读对象)
ObjectOutputStraem类 接下来,先学习ObjectOutputStream流,它也是一个包装流,不能单独使用,需要结合原始的字节输出流使用。
代码如下:将一个User对象写到文件中去
第一步:先准备一个User类,必须让其实现Serializable接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class User implements Serializable { private String loginName; private String userName; private int age; private transient String passWord; public User () { } public User (String loginName, String userName, int age, String passWord) { this .loginName = loginName; this .userName = userName; this .age = age; this .passWord = passWord; } @Override public String toString () { return "User{" + "loginName='" + loginName + '\'' + ", userName='" + userName + '\'' + ", age=" + age + ", passWord='" + passWord + '\'' + '}' ; } }
第二步:再创建ObjectOutputStream流对象,调用writeObject方法对象到文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class Test1ObjectOutputStream { public static void main (String[] args) { try ( ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("io-app2/src/itheima11out.txt" )); ){ User u = new User ("admin" , "张三" , 32 , "666888xyz" ); oos.writeObject(u); System.out.println("序列化对象成功!!" ); } catch (Exception e) { e.printStackTrace(); } } }
注意:写到文件中的对象,是不能用记事本打开看的。因为对象本身就不是文本数据,打开是乱码
怎样才能读懂文件中的对象是什么呢?这里必须用反序列化,自己写代码读。
接下来,学习ObjectInputStream流,它也是一个包装流,不能单独使用,需要结合原始的字节输入流使用。
接着前面的案例,文件中已经有一个Student对象,现在要使用ObjectInputStream读取出来。称之为反序列化。
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Test2ObjectInputStream { public static void main (String[] args) { try ( ObjectInputStream ois = new ObjectInputStream (new FileInputStream ("io-app2/src/itheima11out.txt" )); ){ User u = (User) ois.readObject(); System.out.println(u); } catch (Exception e) { e.printStackTrace(); } } }
补充知识:IO框架 最后,再给同学们补充讲解一个知识,叫做IO框架。它有什么用呢?有同学经常问老师,我们只学习了IO流对文件复制,能不能复制文件夹呀?
当然是可以咯,但是如果让我们自己写复制文件夹的代码需要用到递归,还是比较麻烦的。为了简化对IO操作,由apache开源基金组织提供了一组有关IO流小框架,可以提高IO流的开发效率。
这个框架的名字叫commons-io:其本质是别人写好的一些字节码文件(class文件),打包成了一个jar包。我们只需要把jar包引入到我们的项目中,就可以直接用了。
这里给同学们介绍一个jar包中提供的工具类叫FileUtils,它的部分功能如下,很方便,你一看名字就知道怎么用了。
FileUtils类提供的部分方法展示 说明 public static void copyFile(File srcFile,File destFile) 复制文件。 public static void copyDirectory(File srcDir,File destDir) 复制文件夹 public static void deleteDirectory(File directory) 删除文件夹 public static string readFileToString(File file,string encoding) 读数据 public static void writeStringToFile(File file,string data, String charname, boolean append) 写数据
IOUtils类提供的部分方法展示 说明 public static int copy(InputStream inputstream,outputstream outputstream) 复制文件。 public static int copy(Reader reader,writer writer) 复制文件。 public static void write(String data,outputStream output,string charsetName) 写数据
在写代码之前,先需要引入jar包,具体步骤如下
1 2 3 1. 在模块的目录下,新建一个lib文件夹2. 把jar包复制粘贴到lib文件夹下3. 选择lib下的jar包,右键点击Add As Library,然后就可以用了。
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class CommonsIOTest1 { public static void main (String[] args) throws Exception { FileUtils.copyFile(new File ("io-app2\\src\\itheima01.txt" ), new File ("io-app2/src/a.txt" )); FileUtils.copyDirectory(new File ("D:\\resource\\私人珍藏" ), new File ("D:\\resource\\私人珍藏3" )); FileUtils.deleteDirectory(new File ("D:\\resource\\私人珍藏3" )); Files.copy(Path.of("io-app2\\src\\itheima01.txt" ), Path.of("io-app2\\src\\b.txt" )); System.out.println(Files.readString(Path.of("io-app2\\src\\itheima01.txt" ))); } }
特殊文件、日志技术、多线程 属性文件 特殊文件概述 同学们,前面我们学习了IO流,我们知道IO流是用来读、写文件中的数据。但是我们接触到的文件都是普通的文本文件,普通的文本文件里面的数据是没有任何格式规范的,用户可以随意编写,如下图所示。
像这种普通的文本文件,没有任何规律可言,不方便程序对文件中的数据信息处理。
在以后的Java开发过程中还会遇到一些特殊的文本文件,这些文件是有一些格式要求的,方便程序对文件中的数据进行处理。
比如,后面我们会用到两种特殊的文本文件,一种是properties文件,还有一种是xml文件。如下图所示。
后缀为.properties的文件,称之为属性文件,它可以很方便的存储一些类似于键值对的数据。经常当做软件的配置文件使用。
而xml文件能够表示更加复杂的数据关系,比如要表示多个用户的用户名、密码、家乡、性别等。在后面,也经常当做软件的配置文件使用。
现在,学习特殊的文件主要学习什么呢? 主要学习以下的三点
了解它们的特点、作用
学习使用程序读取它们里面的数据
学习使用程序把数据存储到这些文件里
Properties属性文件 接下来,我们先学习Properties这种属性文件。首先我们要掌握属性文件的格式:
属性文件后缀以.properties
结尾
属性文件里面的每一行都是一个键值对,键和值中间用=隔开。比如: admin=123456
#
表示这样是注释信息,是用来解释这一行配置是什么意思。
每一行末尾不要习惯性加分号,以及空格等字符;不然会把分号,空格会当做值的一部分。
键不能重复,值可以重复
如下图所示
接下来,我们学习如何读取属性文件中的数据。这里需要给同学们,介绍一个来叫Properties.
1 2 3 4 5 1. Properties是什么? Properties是Map接口下面的一个实现类,所以Properties也是一种双列集合,用来存储键值对。 但是一般不会把它当做集合来使用。 2. Properties核心作用? Properties类的对象,用来表示属性文件,可以用来读取属性文件中的键值对。
使用Properties读取属性文件中的键值对 ,需要用到的方法如下。
构造器 说明 public Properties() 用于构建Properties集合对象(空容器)
常用方法 说明 public void load(InputStream is) 通过字节输入流,读取属性文件里的键值对数据 public void load(Reader reader) 通过字符输入流,读取属性文件里的键值对数据 public string getProperty(String key) 根据键获取值(其实就是get方法的效果) public Set stringPropertyNames() 获取全部键的集合(其实就是ketSet方法的效果)
实用Properties读取属性文件的步骤如下
1 2 3 1 、创建一个Properties的对象出来(键值对集合,空容器)2 、调用load(字符输入流/字节输入流)方法,开始加载属性文件中的键值对数据到properties对象中去3 、调用getProperty(键)方法,根据键取值
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class PropertiesTest1 { public static void main (String[] args) throws Exception { Properties properties = new Properties (); System.out.println(properties); properties.load(new FileReader ("properties-xml-log-app\\src\\users.properties" )); System.out.println(properties); System.out.println(properties.getProperty("赵敏" )); System.out.println(properties.getProperty("张无忌" )); Set<String> keys = properties.stringPropertyNames(); for (String key : keys) { String value = properties.getProperty(key); System.out.println(key + "---->" + value); } properties.forEach((k, v) -> { System.out.println(k + "---->" + v); }); } }
使用Properties往属性文件中写键值对 ,需要用到的方法如下
常用方法 说明 public Object setProperty(String key,String value) 保存键值对数据到Properties对象中去。 public void store(OutputStream os,String comments) 把键值对数据,通过字节输出流写出到属性文件里去 public void store(writer w,String comments) 把键值对数据,通过字符输出流写出到属性文件里去
往Properties属性文件中写键值对的步骤如下
1 2 3 4 5 1 、先准备一个.properties属性文件,按照格式写几个键值对1 、创建Properties对象出来,2 、调用setProperty存储一些键值对数据3 、调用store(字符输出流/字节输出流, 注释),将Properties集合中的键和值写到文件中 注意:第二个参数是注释,必须得加;
先准备一个users.properties
属性文件,如下图所示
#以下内容都是用户名和密码。 admin=123456 张无忌=minmin 周芷若=wuji 赵敏=wuji
接下来,编写代码读取上面的属性文件。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class PropertiesTest2 { public static void main (String[] args) throws Exception { Properties properties = new Properties (); properties.setProperty("张无忌" , "minmin" ); properties.setProperty("殷素素" , "cuishan" ); properties.setProperty("张翠山" , "susu" ); properties.store(new FileWriter ("properties-xml-log-app/src/users2.properties" ) , "i saved many users!" ); } }
XML文件 同学们,在上一节我们学习了properties属性文件。接下来我们再学习一种在开发中经常使用的文件,叫做xml文件。我们先来给同学们介绍一下,什么是xml文件,然后再来学习如何读取xml文件中的数据。
XML文件概述 首先,我们来认识一下,什么是XML?
XML是可扩展的标记语言,意思是它是由一些标签组成 的,而这些标签是自己定义的。本质上一种数据格式,可以用来表示复杂的数据关系。
XML文件有如下的特点:
XML中的<标签名>
称为一个标签或者一个元素,一般是成对出现的。
XML中的标签名可以自己定义(可扩展),但是必须要正确的嵌套
XML中只能有一个根标签。
XML标准中可以有属性
XML必须第一行有一个文档声明,格式是固定的<?xml version="1.0" encoding="UTF-8"?>
XML文件必须是以.xml为后缀结尾
如下图所示
接下,同学们可以跟着步骤新建一个XML文件,试试!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?xml version="1.0" encoding="UTF-8" ?> <users > <user id ="1" desc ="第一个用户" > <name > 张无忌</name > <sex > 男</sex > <地址 > 光明顶</地址 > <password > minmin</password > </user > <people > 很多人</people > <user id ="2" > <name > 敏敏</name > <sex > 女</sex > <地址 > 光明顶</地址 > <password > wuji</password > </user > </users >
上面XML文件中的数据格式是最为常见的,标签有属性、文本、还有合理的嵌套。XML文件中除了写以上的数据格式之外,还有一些特殊的字符不能直接写。
像 <,>,&
等这些符号不能出现在标签的文本中,因为标签格式本身就有<>,会和标签格式冲突。
如果标签文本中有这些特殊字符,需要用一些占位符代替。
1 2 3 4 5 < 表示 < > 表示 > & 表示 & ' 表示 ' " 表示 "
1 <data > 3 < 2 & & 5 > 4 </data >
如果在标签文本中,出现大量的特殊字符,不想使用特殊字符,此时可以用CDATA区,格式如下
1 2 3 4 5 <data1 > <![CDATA[ 3 < 2 && 5 > 4 ]]> </data1 >
最后,给同学们聊聊,XML在实际开发中有什么作用?
关于XML是什么,以及XML的格式,还有XML有什么作用,就先认识到这里。
XML解析1 使用程序读取XML文件中的数据,称之为XML解析。这里并不需要我们自己写IO流代码去读取xml文件中的数据。其实有很多开源的,好用的XML解析框架,最知名的是DOM4J(第三方开发的)
由于DOM4J是第三方提供的,所以需要把第三方提供的Jar包导入到自己的项目中来,才可以使用。具体步骤如下:
DOM4J解析XML文件的思想是:文档对象模型(意思是把整个XML文档、每一个标签、每一个属性都等都当做对象来看待)。Dowument对象表示真个XML文档、Element对象表示标签(元素)、Attribute对象表示属性、标签中的内容就是文本
DOM4J解析XML需要用到的方法如下图所示
SAXReader: Dom4j提供的解析器,可以认为是代表整个Dom4j框架 构造器/方法 说明 public SAXReader() 构建Dom4J的解析器对象 public Document read(String url) 把XML文件读成Document对象 public Document read(InputStream is) 通过字节输入流读取XML文件
Document 方法名 说明 Element getRootElement() 获得根元素对象
XML解析的过程,是从根元素开始,从外层往里层解析。 我们先把Document对象,和根元素获取出来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Dom4JTest1 { public static void main (String[] args) throws Exception { SAXReader saxReader = new SAXReader (); Document document = saxReader.read("properties-xml-log-app\\src\\helloworld.xml" ); Element root = document.getRootElement(); System.out.println(root.getName()); } }
XML解析2 获取到XML文件的根元素之后,接下来,就可以用根元素在获取到它里面的子元素(包括子标签、表属性等)。需要用到的方法如下图所示
方法名 说明 public string getName() 得到元素名字 public List elements() 得到当前元素下所有子元素 public list elements(String name) 得到当前元素下指定名字的子元素返回集合 public Element element(String name) 得到当前元素下指定名字的子元素,如果有很多名字相同的返回第一个 public stringattributeValue(String name) 通过属性名直接得到属性值 public String elementText(子元素名) 得到指定名称的子元素的文本 public string getText() 得到文本
接下来,把上面的方法先一个一个的演示一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 public class Dom4JTest1 { public static void main (String[] args) throws Exception { SAXReader saxReader = new SAXReader (); Document document = saxReader.read("properties-xml-log-app\\src\\helloworld.xml" ); Element root = document.getRootElement(); System.out.println(root.getName()); List<Element> elements = root.elements("user" ); for (Element element : elements) { System.out.println(element.getName()); } Element people = root.element("people" ); System.out.println(people.getText()); Element user = root.element("user" ); System.out.println(user.elementText("name" )); System.out.println(user.attributeValue("id" )); Attribute id = user.attribute("id" ); System.out.println(id.getName()); System.out.println(id.getValue()); List<Attribute> attributes = user.attributes(); for (Attribute attribute : attributes) { System.out.println(attribute.getName() + "=" + attribute.getValue()); } System.out.println(user.elementText("name" )); System.out.println(user.elementText("地址" )); System.out.println(user.elementTextTrim("地址" )); System.out.println(user.elementText("password" )); Element data = user.element("data" ); System.out.println(data.getText()); System.out.println(data.getTextTrim()); } }
XML文件写入 在前面我们已经学习了XML解析,意思就是使用程序把XML文件中的数据读取出来。反过来能不能往XML文件中写入数据呢? 答案是可以的。
DOM4J也提供了往XML文件中写标签的方法,但是用起来比较麻烦。这里不建议使用
我们自己使用StringBuilder按照标签的格式拼接,然后再使用BufferedWriter写到XML文件中去就可以了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class Dom4JTest2 { public static void main (String[] args) { StringBuilder sb = new StringBuilder (); sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\r\n" ); sb.append("<book>\r\n" ); sb.append("\t<name>" ).append("从入门到跑路" ).append("</name>\r\n" ); sb.append("\t<author>" ).append("dlei" ).append("</author>\r\n" ); sb.append("\t<price>" ).append(999.99 ).append("</price>\r\n" ); sb.append("</book>" ); try ( BufferedWriter bw = new BufferedWriter (new FileWriter ("properties-xml-log-app/src/book.xml" )); ){ bw.write(sb.toString()); } catch (Exception e) { e.printStackTrace(); } } }
XML约束(了解) 各位小伙伴,关于XML还有最后一个知识需要大家了解一下。这个知识叫做约束XML文件的编写,我讲这个知识的目的是因为同学们以后在开发过程中会遇到这个知识,但是这个代码不需要大家写,了解一下就可以了。
首先,说一些什么是XML约束?
XML约束指的是限制XML文件中的标签或者属性,只能按照规定的格式写。
比如我在项目中,想约束一个XML文件中的标签只能写<书>、<书名>、<作者>、<售价>这几个标签,如果写其他标签就报错。
怎么才能达到上面的效果呢?有两种约束技术,一种是DTD约束、一种是Schame约束。
日志技术 日志概述 好的同学们,接下来我们学习一个将来我们会用到的一个技术,叫做日志技术。首先同学们肯定想知道什么是日志?
想搞清楚什么是日志,其实可以通过下面几个问题来了解的。
系统系统能记住某些数据被谁操作,比如被谁删除了?
想分析用户浏览系统的具体情况,比如挖掘用户的具体喜好?
当系统在开发中或者上线后出现了Bug,崩溃了,该通过什么去分析,定位Bug?
而日志就可以帮我们解决以上的问题。所以日志就好比生活中的日记,日记可以记录生活中的点点滴滴;而程序中的日志,通常就是一个文件,里面记录了程序运行过程中产生的各种数据。
日志技术有如下好处
日志可以将系统执行的信息,方便的记录到指定位置,可以是控制台、可以是文件、可以是数据库中。
日志可以随时以开关的形式控制启停,无需侵入到源代码中去修改。
日志的体系 在上一节给同学们介绍了,什么是日志,日志能干什么。接下来需要给同学们介绍一下有哪些日志技术,以及日志的体系。
大家注意了在行内,其实有很多日志框架给开发者使用。所谓日志框架就是由一些牛人或者第三方公司已经做好的实现代码,后来者就可以直接拿过去使用。
日志框架有很多种,比如有JUL(java.util.logging)、Log4j、logback等。但是这些日志框架如果使用的API方法都不一样的话,使用者的学习成本就很高。为了降低程序员的学习压力,行内提供了一套日志接口,然后所有的日志框架都按照日志接口的API来实现就可以了。
这样程序员只要会一套日志框架,那么其他的也就可以通过用,甚至可以在多套日志框架之间来回切换。比较常用的日志框架,和日志接口的关系如下图所示
这里推荐同学们使用Logback日志框架,也在行业中最为广泛使用的。
Logback日志分为哪几个模块
Logback快速入门 接下来,就带领同学们快速使用一下Logback日志框架,使用Logback记录几条日志信息到文件中去和将日志信息打印在控制台上。
由于Logback是第三方提供的技术,所以首先需要啊将Jar包引入到项目中,具体步骤如下
在资料中找到slftj-api.jar、logback-core.jar、logback-classes.jar
这三个jar包,复制一下
在当前模块下面新建一个lib文件夹,把刚刚复制的三个jar包都粘贴到此处
从资料中找到logback.xml
配置文件,将此文件复制粘贴到src目录下(必须是src目录)
然后就可以开始写代码了,在代码中创建一个日志记录日对象
1 public static final Logger LOGGER = LoggerFactory.getLogger("当前类名" );
开始记录日志,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class LogBackTest { public static final Logger LOGGER = LoggerFactory.getLogger("LogBackTest" ); public static void main (String[] args) { try { LOGGER.info("chu法方法开始执行~~~" ); chu(10 , 0 ); LOGGER.info("chu法方法执行成功~~~" ); } catch (Exception e) { LOGGER.error("chu法方法执行失败了,出现了bug~~~" ); } } public static void chu (int a, int b) { LOGGER.debug("参数a:" + a); LOGGER.debug("参数b:" + b); int c = a / b; LOGGER.info("结果是:" + c); } }
当我们运行程序时,就可以看到控制台记录的日志
同时在文件中,也有一份这样的日志信息。文件在哪里内,从配置文件中去找
打开D:/log/itheima-data.log
看一下文件中是否有记录日志吧!!
关于Logback的快速入门我们就做完了。至于日志的配置文件中,其他的配置是什么含义我们下一节再继续学习。
日志配置文件 Logback提供了一个核心配置文件logback.xml,日志框架在记录日志时会读取配置文件中的配置信息,从而记录日志的形式。具体可以做哪些配置呢?
1 2 3 1. 可以配置日志输出的位置是文件、还是控制台2. 可以配置日志输出的格式3. 还可以配置日志关闭和开启、以及哪些日志输出哪些日志不输出。
如下图所示,控制日志往文件中输出,还是往控制台输出
3.5 配置日志级别
多线程 同学们,接下来我们来学习一个全新而且非常重要的知识,叫做多线程。首先和同学们聊聊什么是线程?线程其实是程序中的一条执行路径。
我们之前写过的程序,其实都是单线程程序,如下图代码,如果前面的for循环没有执行完,for循环下面的代码是不会执行的。
怎样的程序才是多线程程序呢? 如下图所示,12306网站就是支持多线程的,因为同时可以有很多人一起进入网站购票,而且每一个人互不影响。再比如百度网盘,可以同时下载或者上传多个文件。这些程序中其实就有多条执行路径,每一条执行执行路径就是一条线程,所以这样的程序就是多线程程序。
认识了什么是多线程程序,那如何使用Java创建线程呢? Java提供了几种创建线程的方式,下一节再一种一种的学习。
线程创建方式1 Java为开发者提供了一个类叫做Thread,此类的对象用来表示线程。创建线程并执行线程的步骤如下
1 2 3 1. 定义一个子类继承Thread类,并重写run方法2. 创建Thread的子类对象3. 调用start方法启动线程(启动线程后,会自动执行run方法中的代码)
代码如下
1 2 3 4 5 6 7 8 9 10 public class MyThread extends Thread { @Override public void run () { for (int i = 1 ; i <= 5 ; i++) { System.out.println("子线程MyThread输出:" + i); } } }
再定义一个测试类,在测试类中创建MyThread线程对象,并启动线程
1 2 3 4 5 6 7 8 9 10 11 12 13 public class ThreadTest1 { public static void main (String[] args) { Thread t = new MyThread (); t.start(); for (int i = 1 ; i <= 5 ; i++) { System.out.println("主线程main输出:" + i); } } }
打印结果如下图所示,我们会发现MyThread和main线程在相互抢夺CPU的执行权(注意:哪一个线程先执行,哪一个线程后执行,目前我们是无法控制的,每次输出结果都会不一样 )
最后我们还需要注意一点 :不能直接去调用run方法,如果直接调用run方法就不认为是一条线程启动了,而是把Thread当做一个普通对象,此时run方法中的执行的代码会成为主线程的一部分。此时执行结果是这样的。
线程创建方式2 接下来我们学习线程的第二种创建方式。Java为开发者提供了一个Runnable接口,该接口中只有一个run方法,意思就是通过Runnable接口的实现类对象专门来表示线程要执行的任务。具体步骤如下
1 2 3 4 1. 先写一个Runnable接口的实现类,重写run方法(这里面就是线程要执行的代码)2. 再创建一个Runnable实现类的对象3. 创建一个Thread对象,把Runnable实现类的对象传递给Thread4. 调用Thread对象的start()方法启动线程(启动后会自动执行Runnable里面的run方法)
代码如下:先准备一个Runnable接口的实现类
1 2 3 4 5 6 7 8 9 10 11 12 13 public class MyRunnable implements Runnable { @Override public void run () { for (int i = 1 ; i <= 5 ; i++) { System.out.println("子线程输出 ===》" + i); } } }
再写一个测试类,在测试类中创建线程对象,并执行线程
1 2 3 4 5 6 7 8 9 10 11 12 13 public class ThreadTest2 { public static void main (String[] args) { Runnable target = new MyRunnable (); new Thread (target).start(); for (int i = 1 ; i <= 5 ; i++) { System.out.println("主线程main输出 ===》" + i); } } }
运行上面代码,结果如下图所示(注意:没有出现下面交替执行的效果,也是正常的)
1 2 3 4 5 6 7 8 9 10 主线程main输出 ===》1 主线程main输出 ===》2 主线程main输出 ===》3 子线程输出 ===》1 子线程输出 ===》2 子线程输出 ===》3 子线程输出 ===》4 子线程输出 ===》5 主线程main输出 ===》4 主线程main输出 ===》5
线程创建方式2—匿名内部类 同学们注意了,现在这种写法不是新知识。只是将前面第二种方式用匿名内部类改写一下。因为同学们在看别人写的代码时,有可能会看到这种写法。你知道是怎么回事就可以了。
刚刚我们学习的第二种线程的创建方式,需要写一个Runnable接口的实现类,然后再把Runnable实现类的对象传递给Thread对象。
现在我不想写Runnable实现类,于是可以直接创建Runnable接口的匿名内部类对象,传递给Thread对象。
代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public class ThreadTest2_2 { public static void main (String[] args) { Runnable target = new Runnable () { @Override public void run () { for (int i = 1 ; i <= 5 ; i++) { System.out.println("子线程1输出:" + i); } } }; new Thread (target).start(); new Thread (new Runnable () { @Override public void run () { for (int i = 1 ; i <= 5 ; i++) { System.out.println("子线程2输出:" + i); } } }).start(); new Thread (() -> { for (int i = 1 ; i <= 5 ; i++) { System.out.println("子线程3输出:" + i); } }).start(); for (int i = 1 ; i <= 5 ; i++) { System.out.println("主线程main输出:" + i); } } }
线程的创建方式3 接下来,我们学习线程的第三种创建方式。已经有两种了为什么还有要第三种呢? 这样,我们先分析一下前面两种都存在的一个问题。然后再引出第三种可以解决这个问题。
假设线程执行完毕之后有一些数据需要返回,前面两种方式重写的run方法均没有返回结果。
1 2 3 public void run () { ...线程执行的代码... }
JDK5提供了Callable接口和FutureTask类来创建线程,它最大的优点就是有返回值。
在Callable接口中有一个call方法,重写call方法就是线程要执行的代码,它是有返回值的
1 2 3 4 public T call () { ...线程执行的代码... return 结果; }
第三种创建线程的方式,步骤如下
1 2 3 4 5 6 7 8 1. 先定义一个Callable接口的实现类,重写call方法2. 创建Callable实现类的对象3. 创建FutureTask类的对象,将Callable对象传递给FutureTask4. 创建Thread对象,将Future对象传递给Thread5. 调用Thread的start()方法启动线程(启动后会自动执行call方法) 等call()方法执行完之后,会自动将返回值结果封装到FutrueTask对象中 6. 调用FutrueTask对的get()方法获取返回结果
代码如下:先准备一个Callable接口的实现类
1 2 3 4 5 6 7 8 9 10 11 12 13 public class MyThread extends Thread { @Override public void run () { for (int i = 1 ; i <= 5 ; i++) { System.out.println("子线程MyThread输出:" + i); } } }
再定义一个测试类,在测试类中创建线程并启动线程,还要获取返回结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class ThreadTest3 { public static void main (String[] args) throws Exception { Callable<String> call = new MyCallable (100 ); FutureTask<String> f1 = new FutureTask <>(call); new Thread (f1).start(); Callable<String> call2 = new MyCallable (200 ); FutureTask<String> f2 = new FutureTask <>(call2); new Thread (f2).start(); String rs = f1.get(); System.out.println(rs); String rs2 = f2.get(); System.out.println(rs2); } }
多线程 多线程常用方法
下面我们演示一下getName()
、setName(String name)
、currentThread()
、sleep(long time)
这些方法的使用效果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class MyThread extends Thread { public MyThread (String name) { super (name); } @Override public void run () { Thread t = Thread.currentThread(); for (int i = 1 ; i <= 3 ; i++) { System.out.println(t.getName() + "输出:" + i); } } }
再测试类中,创建线程对象,并启动线程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class ThreadTest1 { public static void main (String[] args) { Thread t1 = new MyThread (); t1.setName(String name) t1.start(); System.out.println(t1.getName()); Thread t2 = new MyThread ("2号线程" ); t2.start(); System.out.println(t2.getName()); Thread m = Thread.currentThread(); m.setName("最牛的线程" ); System.out.println(m.getName()); for (int i = 1 ; i <= 5 ; i++) { System.out.println(m.getName() + "线程输出:" + i); } } }
执行上面代码,效果如下图所示,我们发现每一条线程都有自己了名字了。
1号线程 2号线程 最牛的线程 最牛的线程线程输出:1 最牛的线程线程输出:2 最牛的线程线程输出:3 最牛的线程线程输出:4 最牛的线程线程输出:5 2号线程输出:1 2号线程输出:2 2号线程输出:3 1号线程输出:1 1号线程输出:2 1号线程输出:3
最后再演示一下join这个方法是什么效果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class ThreadTest2 { public static void main (String[] args) throws Exception { Thread t1 = new MyThread ("1号线程" ); t1.start(); t1.join(); Thread t2 = new MyThread ("2号线程" ); t2.start(); t2.join(); Thread t3 = new MyThread ("3号线程" ); t3.start(); t3.join(); } }
执行效果是1号线程先执行完,再执行2号线程;2号线程执行完,再执行3号线程;3号线程执行完就结束了。
1号线程输出:1 1号线程输出:2 1号线程输出:3 2号线程输出:1 2号线程输出:2 2号线程输出:3 3号线程输出:1 3号线程输出:2 3号线程输出:3
我们再尝试,把join()方法去掉,再看执行效果。此时你会发现2号线程没有执行完1号线程就执行了(效果是多次运行才出现的,根据个人电脑而异,可能有同学半天也出现不了也是正常的)
3号线程输出:1 3号线程输出:2 3号线程输出:3 2号线程输出:1 看,2号执行没有执行完,1号线程就开始执行了 1号线程输出:1 2号线程输出:2 1号线程输出:2 2号线程输出:3 1号线程输出:3
线程安全问题 各位小伙伴,前面我们已经学习了如何创建线程,以及线程的常用方法。接下来,我们要学习一个在实际开发过程中,使用线程时最重要的一个问题,叫线程安全问题。
线程安全问题概述
线程安全问题指的是,多个线程同时操作同一个共享资源的时候,可能会出现业务安全问题。
下面通过一个取钱的案例给同学们演示一下。案例需求如下
1 场景:小明和小红是一对夫妻,他们有一个共享账户,余额是10 万元,小红和小明同时来取钱,并且2 人各自都在取钱10 万元,可能出现什么问题呢?
如下图所示,小明和小红假设都是一个线程,本类每个线程都应该执行完三步操作,才算是完成的取钱的操作。但是真实执行过程可能是下面这样子的
① 小红线程只执行了判断余额是否足够(条件为true),然后CPU的执行权就被小红线程抢走了。
② 小红线程也执行了判断了余额是否足够(条件也是true), 然后CPU执行权又被小明线程抢走了。
③ 小明线程由于刚才已经判断余额是否足够了,直接执行第2步,吐出了10万元钱,此时共享账户月为0。然后CPU执行权又被小红线程抢走。
④ 小红线程由于刚刚也已经判断余额是否足够了,直接执行第2步,吐出了10万元钱,此时共享账户月为-10万。
你会发现,在这个取钱案例中,两个人把共享账户的钱都取了10万,但问题是只有10万块钱啊!!!
以上取钱案例中的问题,就是线程安全问题的一种体现。
线程安全问题的代码演示 先定义一个共享的账户类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public class Account { private String cardId; private double money; public Account () { } public Account (String cardId, double money) { this .cardId = cardId; this .money = money; } public void drawMoney (double money) { String name = Thread.currentThread().getName(); if (this .money >= money){ System.out.println(name + "来取钱" + money + "成功!" ); this .money -= money; System.out.println(name + "来取钱后,余额剩余:" + this .money); }else { System.out.println(name + "来取钱:余额不足~" ); } } public String getCardId () { return cardId; } public void setCardId (String cardId) { this .cardId = cardId; } public double getMoney () { return money; } public void setMoney (double money) { this .money = money; } }
在定义一个是取钱的线程类
1 2 3 4 5 6 7 8 9 10 11 12 public class DrawThread extends Thread { private Account acc; public DrawThread (Account acc, String name) { super (name); this .acc = acc; } @Override public void run () { acc.drawMoney(100000 ); } }
最后,再写一个测试类,在测试类中创建两个线程对象
1 2 3 4 5 6 7 8 9 public class ThreadTest { public static void main (String[] args) { Account acc = new Account ("ICBC-110" , 100000 ); new DrawThread (acc, "小明" ).start(); new DrawThread (acc, "小红" ).start(); } }
运行程序,执行效果如下。你会发现两个人都取了10万块钱,余额为-10完了。
小红来取钱100000.0成功! 小明来取钱100000.0成功! 小红来取钱后,余额剩余:0.0 小明来取钱后,余额剩余:-100000.0
线程同步方案 为了解决前面的线程安全问题,我们可以使用线程同步思想。同步最常见的方案就是加锁,意思是每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动释放锁,然后其他线程才能再加锁进来。
等小红线程执行完了,把余额改为0,出去了就会释放锁。这时小明线程就可以加锁进来执行,如下图所示。
采用加锁的方案,就可以解决前面两个线程都取10万块钱的问题。怎么加锁呢?Java提供了三种方案
同步代码块 我们先来学习同步代码块。它的作用就是把访问共享数据的代码锁起来,以此保证线程安全。
使用同步代码块,来解决前面代码里面的线程安全问题。我们只需要修改DrawThread类中的代码即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public void drawMoney (double money) { String name = Thread.currentThread().getName(); synchronized (this ) { if (this .money >= money){ System.out.println(name + "来取钱" + money + "成功!" ); this .money -= money; System.out.println(name + "来取钱后,余额剩余:" + this .money); }else { System.out.println(name + "来取钱:余额不足~" ); } } }
此时再运行测试类,观察是否会出现不合理的情况。
最后,再给同学们说一下锁对象如何选择的问题
1 2 3 1. 建议把共享资源作为锁对象, 不要将随便无关的对象当做锁对象2. 对于实例方法,建议使用this 作为锁对象3. 对于静态方法,建议把类的字节码(类名.class)当做锁对象
同步方法 接下来,学习同步方法解决线程安全问题。其实同步方法,就是把整个方法给锁住,一个线程调用这个方法,另一个线程调用的时候就执行不了,只有等上一个线程调用结束,下一个线程调用才能继续执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 public synchronized void drawMoney (double money) { String name = Thread.currentThread().getName(); if (this .money >= money){ System.out.println(name + "来取钱" + money + "成功!" ); this .money -= money; System.out.println(name + "来取钱后,余额剩余:" + this .money); }else { System.out.println(name + "来取钱:余额不足~" ); } }
改完之后,再次运行测试类,观察是否会出现不合理的情况。
接着,再问同学们一个问题,同步方法有没有锁对象?锁对象是谁?
1 2 3 同步方法也是有锁对象,只不过这个锁对象没有显示的写出来而已。 1. 对于实例方法,锁对象其实是this (也就是方法的调用者) 2. 对于静态方法,锁对象时类的字节码对象(类名.class)
最终,总结一下同步代码块和同步方法有什么区别?
1 2 3 1. 不存在哪个好与不好,只是一个锁住的范围大,一个范围小2. 同步方法是将方法中所有的代码锁住3. 同步代码块是将方法中的部分代码锁住
Lock锁 接下来,我们再来学习一种,线程安全问题的解决办法,叫做Lock锁。
Lock锁是JDK5版本专门提供的一种锁对象,通过这个锁对象的方法来达到加锁,和释放锁的目的,使用起来更加灵活。格式如下
1 2 3 4 5 6 1. 首先在成员变量位子,需要创建一个Lock接口的实现类对象(这个对象就是锁对象) private final Lock lk = new ReentrantLock (); 2. 在需要上锁的地方加入下面的代码 lk.lock(); lk.unlock();
使用Lock锁改写前面DrawThread中取钱的方法,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 private final Lock lk = new ReentrantLock ();public void drawMoney (double money) { String name = Thread.currentThread().getName(); try { lk.lock(); if (this .money >= money){ System.out.println(name + "来取钱" + money + "成功!" ); this .money -= money; System.out.println(name + "来取钱后,余额剩余:" + this .money); }else { System.out.println(name + "来取钱:余额不足~" ); } } catch (Exception e) { e.printStackTrace(); } finally { lk.unlock(); } } }
运行程序结果,观察是否有线程安全问题。到此三种解决线程安全问题的办法我们就学习完了。
线程通信(了解) 接下来,我们学习一下线程通信。
首先,什么是线程通信呢?
当多个线程共同操作共享资源时,线程间通过某种方式互相告知自己的状态,以相互协调,避免无效的资源挣抢。
线程通信的常见模式:是生产者与消费者模型
生产者线程负责生成数据
消费者线程负责消费生产者生成的数据
注意:生产者生产完数据后应该让自己等待,通知其他消费者消费;消费者消费完数据之后应该让自己等待,同时通知生产者生成。
比如下面案例中,有3个厨师(生产者线程),两个顾客(消费者线程)。
接下来,我们先分析一下完成这个案例的思路
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 1. 先确定在这个案例中,什么是共享数据? 答:这里案例中桌子是共享数据,因为厨师和顾客都需要对桌子上的包子进行操作。 2. 再确定有那几条线程?哪个是生产者,哪个是消费者? 答:厨师是生产者线程,3 条生产者线程; 顾客是消费者线程,2 条消费者线程 3. 什么时候将哪一个线程设置为什么状态 生产者线程(厨师)放包子: 1 )先判断是否有包子 2 )没有包子时,厨师开始做包子, 做完之后把别人唤醒,然后让自己等待 3 )有包子时,不做包子了,直接唤醒别人、然后让自己等待 消费者线程(顾客)吃包子: 1 )先判断是否有包子 2 )有包子时,顾客开始吃包子, 吃完之后把别人唤醒,然后让自己等待 3 )没有包子时,不吃包子了,直接唤醒别人、然后让自己等待
按照上面分析的思路写代码。先写桌子类,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 public class Desk { private List<String> list = new ArrayList <>(); public synchronized void put () { try { String name = Thread.currentThread().getName(); if (list.size() == 0 ){ list.add(name + "做的肉包子" ); System.out.println(name + "做了一个肉包子~~" ); Thread.sleep(2000 ); this .notifyAll(); this .wait(); }else { this .notifyAll(); this .wait(); } } catch (Exception e) { e.printStackTrace(); } } public synchronized void get () { try { String name = Thread.currentThread().getName(); if (list.size() == 1 ){ System.out.println(name + "吃了:" + list.get(0 )); list.clear(); Thread.sleep(1000 ); this .notifyAll(); this .wait(); }else { this .notifyAll(); this .wait(); } } catch (Exception e) { e.printStackTrace(); } } }
再写测试类,在测试类中,创建3个厨师线程对象,再创建2个顾客对象,并启动所有线程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public class ThreadTest { public static void main (String[] args) { Desk desk = new Desk (); new Thread (() -> { while (true ) { desk.put(); } }, "厨师1" ).start(); new Thread (() -> { while (true ) { desk.put(); } }, "厨师2" ).start(); new Thread (() -> { while (true ) { desk.put(); } }, "厨师3" ).start(); new Thread (() -> { while (true ) { desk.get(); } }, "吃货1" ).start(); new Thread (() -> { while (true ) { desk.get(); } }, "吃货2" ).start(); } }
执行上面代码,运行结果如下:你会发现多个线程相互协调执行,避免无效的资源挣抢。
1 2 3 4 5 6 7 8 9 10 厨师1做了一个肉包子~~ 吃货2吃了:厨师1做的肉包子 厨师3做了一个肉包子~~ 吃货2吃了:厨师3做的肉包子 厨师1做了一个肉包子~~ 吃货1吃了:厨师1做的肉包子 厨师2做了一个肉包子~~ 吃货2吃了:厨师2做的肉包子 厨师3做了一个肉包子~~ 吃货1吃了:厨师3做的肉包子
线程池 线程池概述 各位小伙伴,接下来我们学习一下线程池技术。先认识一下什么是线程池技术? 其实,线程池就是一个可以复用线程的技术 。
要理解什么是线程复用技术,我们先得看一下不使用线程池会有什么问题,理解了这些问题之后,我们在解释线程复用同学们就好理解了。
1 假设:用户每次发起一个请求给后台,后台就创建一个新的线程来处理,下次新的任务过来肯定也会创建新的线程,如果用户量非常大,创建的线程也讲越来越多。然而,创建线程是开销很大的,并且请求过多时,会严重影响系统性能。
而使用线程池,就可以解决上面的问题。如下图所示,线程池内部会有一个容器,存储几个核心线程,假设有3个核心线程,这3个核心线程可以处理3个任务。
但是任务总有被执行完的时候,假设第1个线程的任务执行完了,那么第1个线程就空闲下来了,有新的任务时,空闲下来的第1个线程可以去执行其他任务。依此内推,这3个线程可以不断的复用,也可以执行很多个任务。
所以,线程池就是一个线程复用技术,它可以提高线程的利用率。
创建线程池 在JDK5版本中提供了代表线程池的接口ExecutorService,而这个接口下有一个实现类叫ThreadPoolExecutor类,使用ThreadPoolExecutor类就可以用来创建线程池对象。
下面是它的构造器,参数比较多,不要怕,干就完了^_^。
接下来,用这7个参数的构造器来创建线程池的对象。代码如下
1 2 3 4 5 6 7 8 9 ExecutorService pool = new ThreadPoolExecutor ( 3 , 5 , 8 , TimeUnit.SECONDS, new ArrayBlockingQueue <>(4 ), Executors.defaultThreadFactory(), new ThreadPoolExecutor .CallerRunsPolicy() );
关于线程池,我们需要注意下面的两个问题
临时线程什么时候创建?
1 新任务提交时,发现核心线程都在忙、任务队列满了、并且还可以创建临时线程,此时会创建临时线程。
什么时候开始拒绝新的任务?
1 核心线程和临时线程都在忙、任务队列也满了、新任务过来时才会开始拒绝任务。
线程池执行Runnable任务 创建好线程池之后,接下来我们就可以使用线程池执行任务了。线程池执行的任务可以有两种,一种是Runnable任务;一种是callable任务。下面的execute方法可以用来执行Runnable任务。
方法名称 说明 void execute(Runnable command) 执行 Runnable任务 Future submit(Callable task) 执行callable任务,返回未来任务对象,用于获取线程返回的结果 void shutdown() 等全部任务执行完毕后,再关闭线程池! List shutdownNow() 立刻关闭线程池,停止正在执行的任务,并返回队列中未执行的任务
先准备一个线程任务类
1 2 3 4 5 6 7 8 9 10 11 12 13 public class MyRunnable implements Runnable { @Override public void run () { System.out.println(Thread.currentThread().getName() + " ==> 输出666~~" ); try { Thread.sleep(Integer.MAX_VALUE); } catch (InterruptedException e) { e.printStackTrace(); } } }
下面是执行Runnable任务的代码,注意阅读注释,对照着前面的7个参数理解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 ExecutorService pool = new ThreadPoolExecutor ( 3 , 5 , 8 , TimeUnit.SECONDS, new ArrayBlockingQueue <>(4 ), Executors.defaultThreadFactory(), new ThreadPoolExecutor .CallerRunsPolicy() ); Runnable target = new MyRunnable ();pool.execute(target); pool.execute(target); pool.execute(target); pool.execute(target); pool.execute(target); pool.execute(target); pool.execute(target); pool.execute(target); pool.execute(target); pool.execute(target);
执行上面的代码,结果输出如下
线程池执行Callable任务 接下来,我们学习使用线程池执行Callable任务。callable任务相对于Runnable任务来说,就是多了一个返回值。
执行Callable任务需要用到下面的submit方法
方法名称 说明 void execute(Runnable command) 执行任务/命令,没有返回值,一般用来执行Runnable任务 Future submit(Callable task) 执行任务,返回未来任务对象获取线程结果,一般拿来执行callable任务 void shutdown 等任务执行完毕后关闭线程池 List shutdownNow() 立刻关闭,停止正在执行的任务,并返回队列中未执行的任务
先准备一个Callable线程任务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class MyCallable implements Callable <String> { private int n; public MyCallable (int n) { this .n = n; } @Override public String call () throws Exception { int sum = 0 ; for (int i = 1 ; i <= n; i++) { sum += i; } return Thread.currentThread().getName() + "求出了1-" + n + "的和是:" + sum; } }
再准备一个测试类,在测试类中创建线程池,并执行callable任务。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class ThreadPoolTest2 { public static void main (String[] args) throws Exception { ExecutorService pool = new ThreadPoolExecutor ( 3 , 5 , 8 , TimeUnit.SECONDS, new ArrayBlockingQueue <>(4 ), Executors.defaultThreadFactory(), new ThreadPoolExecutor .CallerRunsPolicy()); Future<String> f1 = pool.submit(new MyCallable (100 )); Future<String> f2 = pool.submit(new MyCallable (200 )); Future<String> f3 = pool.submit(new MyCallable (300 )); Future<String> f4 = pool.submit(new MyCallable (400 )); System.out.println(f1.get()); System.out.println(f2.get()); System.out.println(f3.get()); System.out.println(f4.get()); } }
执行后,结果如下图所示
pool-1-thread-1求出了1-100的和是:5050 pool-1-thread-2求出了1-200的和是:20100 pool-1-thread-3求出了1-300的和是:45150 pool-1-thread-2求出了1-400的和是:80200
线程池工具类(Executors) 有同学可能会觉得前面创建线程池的代码参数太多、记不住,有没有快捷的创建线程池的方法呢?有的。Java为开发者提供了一个创建线程池的工具类,叫做Executors,它提供了方法可以创建各种不能特点的线程池。如下图所示
方法名称 说明 public static ExecutorService newFixedThreadPool(int nThreads) 创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它。
public static ExecutorService newsingleThreadExecutor() 创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程。
public static Executorservice newCachedThreadPool() 线程数量随着任务增加而增加,如果线利任务执行完毕且空闲了60s则会被回收掉。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolsize) 创建—个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务。
接下来,我们演示一下创建固定线程数量的线程池。这几个方法用得不多,所以这里不做过多演示,同学们了解一下就行了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class ThreadPoolTest3 { public static void main (String[] args) throws Exception { ExecutorService pool = Executors.newFixedThreadPool(17 ); Future<String> f1 = pool.submit(new MyCallable (100 )); Future<String> f2 = pool.submit(new MyCallable (200 )); Future<String> f3 = pool.submit(new MyCallable (300 )); Future<String> f4 = pool.submit(new MyCallable (400 )); System.out.println(f1.get()); System.out.println(f2.get()); System.out.println(f3.get()); System.out.println(f4.get()); } }
Executors创建线程池这么好用,为什么不推荐同学们使用呢?原因在这里:看下图,这是《阿里巴巴Java开发手册》提供的强制规范要求。
补充知识 最后,我们再补充几个概念性的知识点,同学们知道这些概念什么意思就可以了。
并发和并行 先学习第一个补充知识点,并发和并行。在讲解并发和并行的含义之前,我们先来了解一下什么是进程、线程?
正常运行的程序(软件)就是一个独立的进程
线程是属于进程,一个进程中包含多个线程
进程中的线程其实并发和并行同时存在(继续往下看)
我们可以打开系统的任务管理器看看(快捷键:Ctrl+Shfit+Esc),自己的电脑上目前有哪些进程。
知道了什么是进程和线程之后,接着我们再来学习并发和并行的含义。
首先,来学习一下什么是并发?
进程中的线程由CPU负责调度执行,但是CPU同时处理线程的数量是优先的,为了保证全部线程都能执行到,CPU采用轮询机制为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。(简单记:并发就是多条线程交替执行)
接下,再来学习一下什么是并行?
并行指的是,多个线程同时被CPU调度执行。如下图所示,多个CPU核心在执行多条线程
最后一个问题,多线程到底是并发还是并行呢?
其实多个线程在我们的电脑上执行,并发和并行是同时存在的。
线程的生命周期 接下来,我们学习最后一个有关线程的知识点,叫做线程的生命周期。所谓生命周期就是线程从生到死的过程中间有哪些状态,以及这些状态之间是怎么切换的。
为了让大家同好的理解线程的生命周期,先用人的生命周期举个例子,人从生到死有下面的几个过程。在人的生命周期过程中,各种状态之间可能会有切换,线程也是一样的。
接下来就来学习线程的生命周期。在Thread类中有一个嵌套的枚举类叫Thread.Status,这里面定义了线程的6中状态。如下图所示
1 2 3 4 5 6 NEW: 新建状态,线程还没有启动 RUNNABLE: 可以运行状态,线程调用了start()方法后处于这个状态 BLOCKED: 锁阻塞状态,没有获取到锁处于这个状态 WAITING: 无限等待状态,线程执行时被调用了wait方法处于这个状态 TIMED_WAITING: 计时等待状态,线程执行时被调用了sleep(毫秒)或者wait(毫秒)方法处于这个状态 TERMINATED: 终止状态, 线程执行完毕或者遇到异常时,处于这个状态。
这几种状态之间切换关系如下图所示
网络编程 网络编程概述 同学们,今天我们学习的课程内容叫网络编程。意思就是编写的应用程序可以与网络上其他设备中的应用程序进行数据交互。
网络编程有什么用呢?这个就不言而喻了,比如我们经常用的微信收发消息就需要用到网络通信的技术、在比如我们打开浏览器可以浏览各种网络、视频等也需要用到网络编程的技术。
我们知道什么是网络编程、也知道网络编程能干什么后了,那Java给我们提供了哪些网络编程的解决方案呢?
Java提供的网络编程的解决方案都是在java.net包下。在正式学习Java网络编程技术之前,我们还需要学习一些网络通信的前置知识理论知识,只有这些前置知识做基础,我们学习网络编程代码编写才起来才能继续下去。
首先和同学们聊聊网络通信的基本架构。通信的基本架构主要有两种形式:一种是CS架构(Client 客户端/Server服务端)、一种是BS架构(Brower 浏览器/Server服务端)。
CS架构的特点: CS架构需要用户在自己的电脑或者手机上安装客户端软件,然后由客户端软件通过网络连接服务器程序,由服务器把数据发给客户端,客户端就可以在页面上看到各种数据了。
这两种结构不管是CS、还是BS都是需要用到网络编程的相关技术。我们学习Java的程序员,以后从事的工作方向主要还是BS架构的。
网络编程三要素 各位小伙伴,我们前面已经知道什么是网络编程了。接下来我们还需要学习一些网络编程的基本概念,才能去编写网络编程的应用程序。
有哪三要素呢?分别是IP地址、端口号、通信协议
IP地址:表示设备在网络中的地址,是网络中设备的唯一标识
端口号:应用程序在设备中唯一的标识
协议:连接和数据在网络中传输的规则。
如下图所示:假设现在要从一台电脑中的微信上,发一句“你愁啥?”到其他电脑的微信上,流程如下
1 2 3 1. 先通过ip地址找到对方的电脑2. 再通过端口号找到对方的电脑上的应用程序3. 按照双方约定好的规则发送、接收数据
IP地址 接下来,我们详细介绍一下IP地址。IP(Ineternet Protocol)全称互联网协议地址,是分配给网络设备的唯一表示。 IP地址分为:IPV4地址、IPV6地址
IPV4地址由32个比特位(4个字节)组成,如果下图所示,但是由于采用二进制太不容易阅读了,于是就将每8位看成一组,把每一组用十进制表示(叫做点分十进制表示法)。所以就有了我们经常看到的IP地址形式,如:192.168.1.66
如果想查看本机的IP地址,可以在命令行窗口,输入ipconfig
命令查看,如下图所示
经过不断的发展,现在越来越多的设备需要联网,IPV4地址已经不够用了,所以扩展出来了IPV6地址。
IPV6采用128位二进制数据来表示(16个字节),号称可以为地球上的每一粒沙子编一个IP地址,
IPV6比较长,为了方便阅读,每16位编成一组,每组采用十六进制数据表示,然后用冒号隔开(称为冒分十六进制表示法),如下图所示
我们在命令行窗口输入ipconfig
命令,同样可以看到ipv6地址,如下图所示
现在的网络设备,一般IPV4和IPV6地址都是支持的。
聊完什么是IP地址和IP地址分类之后,接下来再给大家介绍一下和IP地址相关的一个东西,叫做域名。
我们在浏览器上访问某一个网站是,就需要在浏览器的地址栏输入网址,这个网址的专业说法叫做域名。比如:传智播客的域名是http://www.itcast.cn
。
域名和IP其实是一一对应的,由运营商来管理域名和IP的对应关系。我们在浏览器上敲一个域名时,首先由运营商的域名解析服务,把域名转换为ip地址,再通过IP地址去访问对应的服务器设备。
关于IP地址,还有一个特殊的地址需要我们记住一下。就是我们在学习阶段进行测试时,经常会自己给自己消息,需要用到一个本地回送地址:127.0.0.1
最后给同学们介绍,两个和IP地址相关的命令
1 2 ipconfig: 查看本机的ip地址 pring 域名/ip 检测当前电脑与指定的ip是否连通
ping命令出现以下的提示,说明网络是通过的
InetAddress类 各位小伙伴,在上一节课我们学习了网络编程的三要素之一,IP地址。按照面向对象的设计思想,Java中也有一个类用来表IP地址,这个类是InetAddress类。我们在开发网络通信程序的时候,可能有时候会获取本机的IP地址,以及测试与其他地址是否连通,这个时候就可以使用InetAddress类来完成。下面学习几个InetAddress的方法。
名称 说明 public static InetAddress getLocalHost() 获取本机IP,会以一个inetAddress的对象返回 public static InetAddress getByName(String host) 根据ip地址或者域名,返回一个inetAdress对象 public string getHostName() 获取该ip地址对象对应的主机名。 public string getHostAddress() 获取该ip地址对象中的ip地址信息。 public boolean isReachable(int timeout) 在指定毫秒内,判断主机与该ip对应的主机是否能连通
演示上面几个方法的效果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class InetAddressTest { public static void main (String[] args) throws Exception { InetAddress ip1 = InetAddress.getLocalHost(); System.out.println(ip1.getHostName()); System.out.println(ip1.getHostAddress()); InetAddress ip2 = InetAddress.getByName("www.baidu.com" ); System.out.println(ip2.getHostName()); System.out.println(ip2.getHostAddress()); System.out.println(ip2.isReachable(6000 )); } }
端口号 端口号:指的是计算机设备上运行的应用程序的标识,被规定为一个16位的二进制数据,范围(0~65535)
端口号分为一下几类(了解一下)
周知端口:0~1023,被预先定义的知名应用程序占用(如:HTTP占用80,FTP占用21)
注册端口:1024~49151,分配给用户经常或者某些应用程序
动态端口:49152~65536,之所以称为动态端口,是因为它一般不固定分配给某进程,而是动态分配的。
需要我们注意的是,同一个计算机设备中,不能出现两个应用程序,用同一个端口号
协议 各位同学,前面我们已经学习了IP地址和端口号,但是想要完成数据通信还需要有通信协议。
网络上通信的设备,事先规定的连接规则,以及传输数据的规则被称为网络通信协议。
为了让世界上各种上网设备能够互联互通,肯定需要有一个组织出来,指定一个规则,大家都遵守这个规则,才能进行数据通信。
只要按照OSI网络参考模型制造的设备,就可以在国际互联网上互联互通。其中传输层有两个协议,是我们今天会接触到的(UDP协议、TCP协议)
三次握手如下图所示:目的是确认通信双方,手法消息都是正常没问题的
四次挥手如下图所示:目的是确保双方数据的收发已经完成,没有数据丢失
UDP通信代码(入门案例) 有了网络编程的三要素基础知识之后,我们就可以开始学习编写网络通信的程序了。首先学习基于UDP协议通信的代码编写。
UDP是面向无连接的、不需要确认双方是否存在,所以它是不可靠的协议。Java提供了一个类叫DatagramSocket来完成基于UDP协议的收发数据。使用DatagramSocket收发数据时,数据要以数据包的形式体现,一个数据包限制在64KB以内
具体流程如下图所示:假设我们把DatagramSocket看做是街道两天的人,现在左边的人要扔一盘韭菜到右边,这里的韭菜就是数据,但是数据需要用一个盘子装起来,这里的盘子就是DatagramPacket数据包的意思。通信双方都需要有DatagramSocket(扔、接韭菜人),还需要有DatagramPacket(装韭菜的盘子)
下面我们看一个案例,需要有两个程序,一个表示客户端程序,一个表示服务端程序。
需求:客户端程序发一个字符串数据给服务端,服务端程序接收数据并打印。
客户端程序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class Client { public static void main (String[] args) throws Exception { DatagramSocket socket = new DatagramSocket (7777 ); byte [] bytes = "我是快乐的客户端,我爱你abc" .getBytes(); DatagramPacket packet = new DatagramPacket (bytes, bytes.length , InetAddress.getLocalHost(), 6666 ); socket.send(packet); System.out.println("客户端数据发送完毕~~~" ); socket.close(); } }
服务端程序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class Server { public static void main (String[] args) throws Exception { System.out.println("----服务端启动----" ); DatagramSocket socket = new DatagramSocket (6666 ); byte [] buffer = new byte [1024 * 64 ]; DatagramPacket packet = new DatagramPacket (buffer, buffer.length); socket.receive(packet); int len = packet.getLength(); String rs = new String (buffer, 0 , len); System.out.println(rs); System.out.println(packet.getAddress().getHostAddress()); System.out.println(packet.getPort()); socket.close(); } }
UDP通信代码(多发多收) 刚才的案例,我们只能客户端发一次,服务端接收一次就结束了。下面我们想把这个代码改进一下,
需求:实现客户端不断的发数据,而服务端能不断的接收数据,客户端发送exit时客户端程序退出。
客户端程序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public class Client { public static void main (String[] args) throws Exception { DatagramSocket socket = new DatagramSocket (); Scanner sc = new Scanner (System.in); while (true ) { System.out.println("请说:" ); String msg = sc.nextLine(); if ("exit" .equals(msg)){ System.out.println("欢迎下次光临!退出成功!" ); socket.close(); break ; } byte [] bytes = msg.getBytes(); DatagramPacket packet = new DatagramPacket (bytes, bytes.length , InetAddress.getLocalHost(), 6666 ); socket.send(packet); } } }
服务端程序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class Server { public static void main (String[] args) throws Exception { System.out.println("----服务端启动----" ); DatagramSocket socket = new DatagramSocket (6666 ); byte [] buffer = new byte [1024 * 64 ]; DatagramPacket packet = new DatagramPacket (buffer, buffer.length); while (true ) { socket.receive(packet); int len = packet.getLength(); String rs = new String (buffer, 0 , len); System.out.println(rs); System.out.println(packet.getAddress().getHostAddress()); System.out.println(packet.getPort()); System.out.println("--------------------------------------" ); } } }
TCP通信(一发一收) 学习完UDP通信的代码编写之后,接下来我们学习TCP通信的代码如何编写。Java提供了一个java.net.Socket类来完成TCP通信。
我们先讲一下Socket完成TCP通信的流程,再讲代码怎么编写就很好理解了。如下图所示
当创建Socket对象时,就会在客户端和服务端创建一个数据通信的管道,在客户端和服务端两边都会有一个Socket对象来访问这个通信管道。
现在假设客户端要发送一个“在一起”给服务端,客户端这边先需要通过Socket对象获取到一个字节输出流,通过字节输出流写数据到服务端
然后服务端这边通过Socket对象可以获取字节输入流,通过字节输入流就可以读取客户端写过来的数据,并对数据进行处理。
服务端处理完数据之后,假设需要把“没感觉”发给客户端端,那么服务端这边再通过Socket获取到一个字节输出流,将数据写给客户端
客户端这边再获取输入流,通过字节输入流来读取服务端写过来的数据。
TCP客户端 下面我们写一个客户端,用来往服务端发数据。由于原始的字节流不是很好用,这里根据我的经验,我原始的OutputStream包装为DataOutputStream是比较好用的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class Client { public static void main (String[] args) throws Exception { Socket socket = new Socket ("127.0.0.1" , 8888 ); OutputStream os = socket.getOutputStream(); DataOutputStream dos = new DataOutputStream (os); dos.writeUTF("在一起,好吗?" ); dos.close(); socket.close(); } }
TCP服务端 上面我们只是写了TCP客户端,还没有服务端,接下来我们把服务端写一下。这里的服务端用来接收客户端发过来的数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class Server { public static void main (String[] args) throws Exception { System.out.println("-----服务端启动成功-------" ); ServerSocket serverSocket = new ServerSocket (8888 ); Socket socket = serverSocket.accept(); InputStream is = socket.getInputStream(); DataInputStream dis = new DataInputStream (is); String rs = dis.readUTF(); System.out.println(rs); System.out.println(socket.getRemoteSocketAddress()); dis.close(); socket.close(); } }
TCP通信(多发多收) 到目前为止,我们已经完成了客户端发送消息、服务端接收消息,但是客户端只能发一次,服务端只能接收一次。现在我想要客户端能过一直发消息,服务端能够一直接收消息。
下面我们把客户端代码改写一下,采用键盘录入的方式发消息,为了让客户端能够一直发,我们只需要将发送消息的代码套一层循环就可以了,当用户输入exit时,客户端退出循环并结束客户端。
6.1 TCP客户端 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public class Client { public static void main (String[] args) throws Exception { Socket socket = new Socket ("127.0.0.1" , 8888 ); OutputStream os = socket.getOutputStream(); DataOutputStream dos = new DataOutputStream (os); Scanner sc = new Scanner (System.in); while (true ) { System.out.println("请说:" ); String msg = sc.nextLine(); if ("exit" .equals(msg)){ System.out.println("欢迎您下次光临!退出成功!" ); dos.close(); socket.close(); break ; } dos.writeUTF(msg); dos.flush(); } } }
TCP服务端 为了让服务端能够一直接收客户端发过来的消息,服务端代码也得改写一下。我们只需要将读取数据的代码加一个循环就可以了。
但是需要我们注意的时,如果客户端Socket退出之后,就表示连接客户端与服务端的数据通道被关闭了,这时服务端就会出现异常。服务端可以通过出异常来判断客户端下线了,所以可以用try…catch把读取客户端数据的代码套一起来,catch捕获到异常后,打印客户端下线。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public class Server { public static void main (String[] args) throws Exception { System.out.println("-----服务端启动成功-------" ); ServerSocket serverSocket = new ServerSocket (8888 ); Socket socket = serverSocket.accept(); InputStream is = socket.getInputStream(); DataInputStream dis = new DataInputStream (is); while (true ) { try { String rs = dis.readUTF(); System.out.println(rs); } catch (Exception e) { System.out.println(socket.getRemoteSocketAddress() + "离线了!" ); dis.close(); socket.close(); break ; } } } }
TCP通信(多线程改进) 上一个案例中我们写的服务端程序只能和一个客户端通信,如果有多个客户端连接服务端,此时服务端是不支持的。
为了让服务端能够支持多个客户端通信,就需要用到多线程技术。具体的实现思路如下图所示:每当有一个客户端连接服务端,在服务端这边就为Socket开启一条线程取执行读取数据的操作,来多少个客户端,就有多少条线程。按照这样的设计,服务端就可以支持多个客户端连接了。
按照上面的思路,改写服务端代码。
多线程改进 首先,我们需要写一个服务端的读取数据的线程类,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class ServerReaderThread extends Thread { private Socket socket; public ServerReaderThread (Socket socket) { this .socket = socket; } @Override public void run () { try { InputStream is = socket.getInputStream(); DataInputStream dis = new DataInputStream (is); while (true ){ try { String msg = dis.readUTF(); System.out.println(msg); } catch (Exception e) { System.out.println("有人下线了:" + socket.getRemoteSocketAddress()); dis.close(); socket.close(); break ; } } } catch (Exception e) { e.printStackTrace(); } } }
接下来,再改写服务端的主程序代码,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class Server { public static void main (String[] args) throws Exception { System.out.println("-----服务端启动成功-------" ); ServerSocket serverSocket = new ServerSocket (8888 ); while (true ) { Socket socket = serverSocket.accept(); System.out.println("有人上线了:" + socket.getRemoteSocketAddress()); new ServerReaderThread (socket).start(); } } }
案例拓展(群聊) 接着前面的案例,下面我们案例再次拓展一下,这个并不需要同学们必须掌握,主要是为了锻炼同学们的编程能力、和编程思维。
我们想把刚才的案例,改进成全能够实现群聊的效果,就是一个客户端发的消息,其他的每一个客户端都可以收到。
刚才我们写的多个客户端可以往服务端发现消息,但是客户端和客户端是不能直接通信的。想要试下全群聊的效果,我们还是必须要有服务端在中间做中转。 具体实现方案如下图所示:
我们可以在服务端创建一个存储Socket的集合,每当一个客户端连接服务端,就可以把客户端Socket存储起来;当一个客户端给服务端发消息时,再遍历集合通过每个Socket将消息再转发给其他客户端。
下面我们改造服务端代码,由于服务端读取数据是在线程类中完成的,所以我们改SerReaderThread
类就可以了。服务端的主程序不用改。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public class ServerReaderThread extends Thread { private Socket socket; public ServerReaderThread (Socket socket) { this .socket = socket; } @Override public void run () { try { InputStream is = socket.getInputStream(); DataInputStream dis = new DataInputStream (is); while (true ){ try { String msg = dis.readUTF(); System.out.println(msg); sendMsgToAll(msg); } catch (Exception e) { System.out.println("有人下线了:" + socket.getRemoteSocketAddress()); Server.onLineSockets.remove(socket); dis.close(); socket.close(); break ; } } } catch (Exception e) { e.printStackTrace(); } } private void sendMsgToAll (String msg) throws IOException { for (Socket onLineSocket : Server.onLineSockets) { OutputStream os = onLineSocket.getOutputStream(); DataOutputStream dos = new DataOutputStream (os); dos.writeUTF(msg); dos.flush(); } } }
BS架构程序(简易版) 前面我们所写的代码都是基于CS架构的。我们说网络编程还可以编写BS架构的程序,为了让同学们体验一下BS架构通信,这里我们写一个简易版的程序。仅仅只是体验下一,后期我们会详细学习BS架构的程序如何编写。
BS架构程序的实现原理,如下图所示:不需要开发客户端程序,此时浏览器就相当于是客户端,此时我们只需要写服务端程序就可以了。
在BS结构的程序中,浏览器和服务器通信是基于HTTP协议来完成的,浏览器给客户端发送数据需要按照HTTP协议规定好的数据格式发给服务端,服务端返回数据时也需要按照HTTP协议规定好的数据给是发给浏览器,只有这两双方才能完成一次数据交互。
客户端程序不需要我们编写(浏览器就是),所以我们只需要写服务端就可以了。
服务端给客户端响应数据的数据格式(HTTP协议规定数据格式)如下图所示:左图是数据格式,右图是示例。
接下来,我们写一个服务端程序按照右图示例的样子,给浏览器返回数据。注意:数据是由多行组成的,必须按照规定的格式来写。
服务端程序 先写一个线程类,用于按照HTTP协议的格式返回数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class ServerReaderThread extends Thread { private Socket socket; public ServerReaderThread (Socket socket) { this .socket = socket; } @Override public void run () { try { OutputStream os = socket.getOutputStream(); PrintStream ps = new PrintStream (os); ps.println("HTTP/1.1 200 OK" ); ps.println("Content-Type:text/html;charset=UTF-8" ); ps.println(); ps.println("<div style='color:red;font-size:120px;text-align:center'>黑马程序员666<div>" ); ps.close(); socket.close(); } catch (Exception e) { e.printStackTrace(); } } }
再写服务端的主程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class Server { public static void main (String[] args) throws Exception { System.out.println("-----服务端启动成功-------" ); ServerSocket serverSocket = new ServerSocket (8080 ); while (true ) { Socket socket = serverSocket.accept(); System.out.println("有人上线了:" + socket.getRemoteSocketAddress()); new ServerReaderThread (socket).start(); } } }
服务端主程序用线程池改进 为了避免服务端创建太多的线程,可以把服务端用线程池改进,提高服务端的性能。
先写一个给浏览器响应数据的线程任务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class ServerReaderRunnable implements Runnable { private Socket socket; public ServerReaderRunnable (Socket socket) { this .socket = socket; } @Override public void run () { try { OutputStream os = socket.getOutputStream(); PrintStream ps = new PrintStream (os); ps.println("HTTP/1.1 200 OK" ); ps.println("Content-Type:text/html;charset=UTF-8" ); ps.println(); ps.println("<div style='color:red;font-size:120px;text-align:center'>黑马程序员666<div>" ); ps.close(); socket.close(); } catch (Exception e) { e.printStackTrace(); } } }
再改写服务端的主程序,使用ThreadPoolExecutor创建一个线程池,每次接收到一个Socket就往线程池中提交任务就行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class Server { public static void main (String[] args) throws Exception { System.out.println("-----服务端启动成功-------" ); ServerSocket serverSocket = new ServerSocket (8080 ); ThreadPoolExecutor pool = new ThreadPoolExecutor (16 * 2 , 16 * 2 , 0 , TimeUnit.SECONDS, new ArrayBlockingQueue <>(8 ) , Executors.defaultThreadFactory(), new ThreadPoolExecutor .AbortPolicy()); while (true ) { Socket socket = serverSocket.accept(); pool.execute(new ServerReaderRunnable (socket)); } } }
单元测试、反射 Java主要的知识我们其实已经学习得差不多了。今天同学们再把单元测试、反射、注解、动态代理学习完。
首先,我们进入单元测试的学习。
单元测试 单元测试快速入门 所谓单元测试,就是针对最小的功能单元,编写测试代码对其进行正确性测试。
我们想想,咱们之前是怎么进行测试的呢?
比如说我们写了一个学生管理系统,有添加学生、修改学生、删除学生、查询学生等这些功能。要对这些功能这几个功能进行测试,我们是在main方法中编写代码来测试的。
但是在main方法中写测试代码有如下的几个问题,如下图所示:
为了测试更加方便,有一些第三方的公司或者组织提供了很好用的测试框架,给开发者使用。这里给同学们介绍一种Junit测试框架。
Junit是第三方公司开源出来的,用于对代码进行单元测试的工具(IDEA已经集成了junit框架)。相比于在main方法中测试有如下几个优点。
我们知道单元测试是什么之后,接下来带领同学们使用一下。由于Junit是第三方提供的,所以我们需要把jar包导入到我们的项目中,才能使用,具体步骤如下图所示:
接下来,我们就按照上面的步骤,来使用一下.
先准备一个类,假设写了一个StringUtil工具类,代码如下
1 2 3 4 5 public class StringUtil { public static void printNumber (String name) { System.out.println("名字长度:" +name.length()); } }
接下来,写一个测试类,测试StringUtil工具类中的方法能否正常使用。
1 2 3 4 5 6 7 public class StringUtilTest { @Test public void testPrintNumber () { StringUtil.printNumber("admin" ); StringUtil.printNumber(null ); } }
写完代码之后,我们会发现测试方法左边,会有一个绿色的三角形按钮。点击这个按钮,就可以运行测试方法。
单元测试断言 接下来,我们学习一个单元测试的断言机制。所谓断言:意思是程序员可以预测程序的运行结果,检查程序的运行结果是否与预期一致。
我们在StringUtil类中新增一个测试方法
1 2 3 4 5 6 public static int getMaxIndex (String data) { if (data == null ){ return -1 ; } return data.length(); }
接下来,我们在StringUtilTest类中写一个测试方法
1 2 3 4 5 6 7 8 9 10 11 12 13 public class StringUtilTest { @Test public void testGetMaxIndex () { int index1 = StringUtil.getMaxIndex(null ); System.out.println(index1); int index2 = StringUtil.getMaxIndex("admin" ); System.out.println(index2); Assert.assertEquals("方法内部有Bug" ,4 ,index2); } }
运行测试方法,结果如下图所示,表示我们预期值与实际值不一致
Junit框架的常用注解 同学们,刚才我们以及学习了@Test注解,可以用来标记一个方法为测试方法,测试才能启动执行。
除了@Test注解,还有一些其他的注解,我们要知道其他注解标记的方法什么时候执行,以及其他注解在什么场景下可以使用。
注解 说明 @Test 测试类中的方法必须用它修饰才能成为测试方法,才能启动执行 @Before 用来修饰一个实例方法,该方法会在每一个测试方法执行之前执行一次。 @After 用来修饰一个实例方法,该方法会在每一个测试方法执行之后执行一次。 @BeforeClass 用来修饰一个静态方法,该方法会在所有测试方法之前只执行一次。 @AfterClass 用来修饰一个静态方法,该方法会在所有测试方法之后只执行一次。
接下来,我们演示一下其他注解的使用。我们在StringUtilTest测试类中,再新增几个测试方法。代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class StringUtilTest { @Before public void test1 () { System.out.println("--> test1 Before 执行了" ); } @BeforeClass public static void test11 () { System.out.println("--> test11 BeforeClass 执行了" ); } @After public void test2 () { System.out.println("--> test2 After 执行了" ); } @AfterCalss public static void test22 () { System.out.println("--> test22 AfterCalss 执行了" ); } }
执行上面的测试类,结果如下图所示,观察执行结果特点如下
1 2 3 4 1. 被@BeforeClass 标记的方法,执行在所有方法之前2. 被@AfterCalss 标记的方法,执行在所有方法之后3. 被@Before 标记的方法,执行在每一个@Test 方法之前4. 被@After 标记的方法,执行在每一个@Test 方法之后
我们现在已经知道每一个注解的作用了,那他们有什么用呢?应用场景在哪里?
我们来看一个例子,假设我想在每个测试方法中使用Socket对象,并且用完之后,需要把Socket关闭。代码就可以按照下面的结构来设计
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class StringUtilTest { private static Socket socket; @Before public void test1 () { System.out.println("--> test1 Before 执行了" ); } @BeforeClass public static void test11 () { System.out.println("--> test11 BeforeClass 执行了" ); socket = new Socket (); } @After public void test2 () { System.out.println("--> test2 After 执行了" ); } @AfterCalss public static void test22 () { System.out.println("--> test22 AfterCalss 执行了" ); socket.close(); } }
最后,我们再补充一点。前面的注解是基于Junit4版本的,再Junit5版本中对注解作了更新,但是作用是一样的。所以这里就不做演示了
注解 说明 @Test 测试类中的方法必须用它修饰才能成为测试方法,才能启动执行 @Before 用来修饰一个实例方法,该方法会在每一个测试方法执行之前执行一次。 @After 用来修饰一个实例方法,该方法会在每一个测试方法执行之后执行一次。 @BeforeClass 用来修饰一个静态方法,该方法会在所有测试方法之前只执行一次。 @AfterClass 用来修饰一个静态方法,该方法会在所有测试方法之后只执行一次。
反射 各位小伙伴,接下来我们要学习反射技术。在学习反射之前,有几个点需要给同学们提前交代一下,接下来我们学习的反射、动态代理、注解等知识点,在以后开发中极少用到,这些技术都是以后学习框架、或者做框架的底层源码。给同学们讲这些技术的目的,是为了以后我们理解框架、或者自己开发框架给别人用作铺垫的。同时由于这些技术非常抽象,所以按照国际惯例,我们都会采用先带着大家充分的认识它们,然后再了解其作用和应用场景。
接下来,我们就需要带着同学们认识一下什么是反射。其实API文档中对反射有详细的说明,我们去了解一下。在java.lang.reflect包中对反射的解释如下图所示
翻译成人话就是:反射技术,指的是加载类的字节码到内存,并以编程的方法解刨出类中的各个成分(成员变量、方法、构造器等)。
反射有啥用呢?其实反射是用来写框架用的,但是现阶段同学们对框架还没有太多感觉。为了方便理解,我给同学们看一个我们见过的例子:平时我们用IDEA开发程序时,用对象调用方法,IDEA会有代码提示,idea会将这个对象能调用的方法都给你列举出来,供你选择,如果下图所示
问题是IDEA怎么知道这个对象有这些方法可以调用呢? 原因是对象能调用的方法全都来自于类,IDEA通过反射技术就可以获取到类中有哪些方法,并且把方法的名称以提示框的形式显示出来,所以你能看到这些提示了。
那记事本写代码为什么没有提示呢? 因为技术本软件没有利用反射技术开发这种代码提示的功能,哈哈!!
好了,认识了反射是什么之后,接下来我还想给同学们介绍一下反射具体学什么?
因为反射获取的是类的信息,那么反射的第一步首先获取到类才行。由于Java的设计原则是万物皆对象,获取到的类其实也是以对象的形式体现的,叫字节码对象 ,用Class类来表示。获取到字节码对象之后,再通过字节码对象就可以获取到类的组成成分了,这些组成成分其实也是对象,其中每一个成员变量用Field类的对象来表示 、每一个成员方法用Method类的对象来表示 ,每一个构造器用Constructor类的对象来表示 。
如下图所示:
获取类的字节码 反射的第一步:是将字节码加载到内存,我们需要获取到的字节码对象。
比如有一个Student类,获取Student类的字节码代码有三种写法。不管用哪一种方式,获取到的字节码对象其实是同一个。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Test1Class { public static void main (String[] args) { Class c1 = Student.class; System.out.println(c1.getName()); System.out.println(c1.getSimpleName()); Class c2 = Class.forName("com.itheima.d2_reflect.Student" ); System.out.println(c1 == c2); Student s = new Student (); Class c3 = s.getClass(); System.out.println(c2 == c3); } }
获取类的构造器 我们已经可以获取到类的字节码对象了。接下来,我们学习一下通过字节码对象获取构造器,并使用构造器创建对象。
获取构造器,需要用到Class类提供的几个方法,如下图所示:
方法 说明 Constructor>[] getConstructors()
获取全部构造器(只能获取public修饰的)
Constructor>[] getDeclaredconstructors() 获取全部构造器(只要存在就能拿到) constructor getConstructor(class>... parameterTypes)
获取某个构造器(只能获取public修饰的)
Constructor getDeclaredConstructor(Class>…parameterTypes) 获取某个构造器(只要存在就能拿到)
想要快速记住这个方法的区别,给同学们说一下这些方法的命名规律,按照规律来记就很方便了。
1 2 3 4 get:获取 Declared: 有这个单词表示可以获取任意一个,没有这个单词表示只能获取一个public 修饰的 Constructor: 构造方法的意思 后缀s: 表示可以获取多个,没有后缀s只能获取一个
话不多少,上代码。假设现在有一个Cat类,里面有几个构造方法,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 public class Cat { private String name; private int age; public Cat () { } private Cat (String name, int age) { } }
接下来,我们写一个测试方法,来测试获取类中所有的构造器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Test2Constructor (){ @Test public void testGetConstructors () { Class c = Cat.class; Constructor[] constructors = c.getDeclaredConstructors(); for (Constructor constructor: constructors){ System.out.println(constructor.getName()+"---> 参数个数:" +constructor.getParameterCount()); } } }
运行测试方法打印结果如下
刚才演示的是获取Cat类中所有的构造器,接下来,我们演示单个构造器试一试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class Test2Constructor (){ @Test public void testGetConstructor () { Class c = Cat.class; Constructor constructor1 = c.getConstructor(); System.out.println(constructor1.getName()+"---> 参数个数:" +constructor1.getParameterCount()); Constructor constructor2 = c.getDeclaredConstructor(String.class,int .class); System.out.println(constructor2.getName()+"---> 参数个数:" +constructor1.getParameterCount()); } }
打印结果如下
反射获取构造器的作用 同学们,刚才上一节我们已经获取到了Cat类中的构造器。获取到构造器后,有什么作用呢?
其实构造器的作用:初始化对象并返回 。
这里我们需要用到如下的两个方法,注意:这两个方法时属于Constructor的,需要用Constructor对象来调用。
constructor提供的方法 说明 T RewInstance(object. . . initargs) 调用此构造器对象表示的构造器,并传入参数,完成对象的初始化并返回 public void setAccessible(boolean flag) 设置为true,表示禁止检查访问控制(暴力反射)
如下图所示,constructor1和constructor2分别表示Cat类中的两个构造器。现在我要把这两个构造器执行起来
由于构造器是private修饰的,先需要调用setAccessible(true)
表示禁止检查访问控制,然后再调用newInstance(实参列表)
就可以执行构造器,完成对象的初始化了。
代码如下:为了看到构造器真的执行, 故意在两个构造器中分别加了两个打印语句
代码的执行结果如下图所示:
反射获取成员变量&使用 同学们,上一节我们已经学习了获取类的构造方法并使用。接下来,我们再学习获取类的成员变量,并使用。
其实套路是一样的,在Class类中提供了获取成员变量的方法,如下图所示。
方法 说明 public Field[] getFields() 获取类的全部成员变量(只能获取public修饰的) public Field[] getDeclaredFields() 获取类的全部成员变量(只要存在就能拿到) public Field getField(string name) 获取类的某个成员变量(只能获取public修饰的) public Field getDeclaredField(string name) 获取类的某个成员变量(只要存在就能拿到)
这些方法的记忆规则,如下
1 2 3 4 get:获取 Declared: 有这个单词表示可以获取任意一个,没有这个单词表示只能获取一个public 修饰的 Field: 成员变量的意思 后缀s: 表示可以获取多个,没有后缀s只能获取一个
假设有一个Cat类它有若干个成员变量,用Class类提供 的方法将成员变量的对象获取出来。
执行完上面的代码之后,我们可以看到控制台上打印输出了,每一个成员变量的名称和它的类型。
在Filed类中提供给给成员变量赋值和获取值的方法,如下图所示。
方法 说明 void set(object obj, object value) : 赋值 object get(object obj) 取值 public void setAccessible(boolean flag) 设置为true,表示禁止检查访问控制(暴力反射)
再次强调一下设置值、获取值的方法时Filed类的需要用Filed类的对象来调用,而且不管是设置值、还是获取值,都需要依赖于该变量所属的对象。代码如下
执行代码,控制台会有如下的打印
反射获取成员方法 各位同学,上面几节我们已经学习了反射获取构造方法、反射获取成员变量,还剩下最后一个就是反射获取成员方法并使用了。
在Java中反射包中,每一个成员方法用Method对象来表示,通过Class类提供的方法可以获取类中的成员方法对象。如下下图所示
方法 说明 Method[] getMethods() 获取类的全部成员方法(只能获取public修饰的) Method[] getDeclaredMethods() 获取类的全部成员方法(只要存在就能拿到) Method getMethod( string name,class<… parameterTypes) 获取类的某个成员方法(只能获取public修饰的) Method getDeclaredMethod(String name,class<?>… parameterTypes) 获取类的某个成员方法(只要存在就能拿到)
接下来我们还是用代码演示一下:假设有一个Cat类,在Cat类中红有若干个成员方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public class Cat { private String name; private int age; public Cat () { System.out.println("空参数构造方法执行了" ); } private Cat (String name, int age) { System.out.println("有参数构造方法执行了" ); this .name=name; this .age=age; } private void run () { System.out.println("(>^ω^<)喵跑得贼快~~" ); } public void eat () { System.out.println("(>^ω^<)喵爱吃猫粮~" ); } private String eat (String name) { return "(>^ω^<)喵爱吃:" +name; } public void setName (String name) { this .name=name; } public String getName () { return name; } public void setAge (int age) { this .age=age; } public int getAge () { return age; } }
接下来,通过反射获取Cat类中所有的成员方法,每一个成员方法都是一个Method对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Test3Method { public static void main (String[] args) { Class c = Cat.class; Method[] methods = c.getDecalaredMethods(); for (Method method : methods){ System.out.println(method.getName()+"-->" +method.getParameterCount()+"-->" +method.getReturnType()); } } }
执行上面的代码,运行结果如下图所示:打印输出每一个成员方法的名称、参数格式、返回值类型
也能获取单个指定的成员方法,如下图所示
获取到成员方法之后,有什么作用呢?
在Method类中提供了方法,可以将方法自己执行起来。
Method提供的方法 说明 public object invoke(object obj, object. . . args) 触发某个对象的该方法执行。 7:-..idetccessible(boolean flag) 设置为true,表示禁止检查访问控制(暴力反射)
下面我们演示一下,把run()
方法和eat(String name)
方法执行起来。看分割线之下的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class Test3Method { public static void main (String[] args) { Class c = Cat.class; Method[] methods = c.getDecalaredMethods(); for (Method method : methods){ System.out.println(method.getName()+"-->" +method.getParameterCount()+"-->" +method.getReturnType()); } System.out.println("-----------------------" ); Method run = c.getDecalaredMethod("run" ); Cat cat = new Cat (); run.setAccessible(true ); Object rs1 = run.invoke(cat); System.out.println(rs1) Method eat = c.getDeclaredMethod("eat" ,String.class); eat.setAccessible(true ); Object rs2 = eat.invoke(cat,"鱼儿" ); System.out.println(rs2) } }
打印结果如下图所示:run()方法执行后打印猫跑得贼快~~
,返回null
; eat()方法执行完,直接返回猫最爱吃:鱼儿
反射的应用 按照前面我们学习反射的套路,我们已经充分认识了什么是反射,以及反射的核心作用是用来获取类的各个组成部分并执行他们。但是由于同学们的经验有限,对于反射的具体应用场景还是很难感受到的(这个目前没有太好的办法,只能慢慢积累,等经验积累到一定程度,就会豁然开朗了)。
我们一直说反射使用来写框架的,接下来,我们就写一个简易的框架,简单窥探一下反射的应用。反射其实是非常强大的,这个案例也仅仅值小试牛刀。
需求是让我们写一个框架,能够将任意一个对象的属性名和属性值写到文件中去。不管这个对象有多少个属性,也不管这个对象的属性名是否相同。
分析一下该怎么做
1 2 3 4 5 6 7 8 9 1. 先写好两个类,一个Student类和Teacher类2. 写一个ObjectFrame类代表框本架 在ObjectFrame类中定义一个saveObject(Object obj)方法,用于将任意对象存到文件中去 参数:Object obj: 就表示要存入文件中的对象 3. 编写方法内部的代码,往文件中存储对象的属性名和属性值 1 )参数obj对象中有哪些属性,属性名是什么实现值是什么,中有对象自己最清楚。 2 )接着就通过反射获取类的成员变量信息了(变量名、变量值) 3 )把变量名和变量值写到文件中去
写一个ObjectFrame表示自己设计的框架,代码如下图所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class ObjectFrame { public static void saveObject (Object obj) throws Exception{ PrintStream ps = new PrintStream (new FileOutputStream ("模块名\\src\\data.txt" ,true )); Class c = obj.getClass(); ps.println("---------" +class.getSimpleName()+"---------" ); Field[] fields = c.getDeclaredFields(); for (Field field : fields){ String name = field.getName(); Object value = field.get(obj)+"" ; ps.println(name); } ps.close(); } }
使用自己设计的框架,往文件中写入Student对象的信息和Teacher对象的信息。
先准备好Student类和Teacher类
1 2 3 4 5 6 7 public class Student { private String name; private int age; private char sex; private double height; private String hobby; }
1 2 3 4 public class Teacher { private String name; private double salary; }
创建一个测试类,在测试中类创建一个Student对象,创建一个Teacher对象,用ObjectFrame的方法把这两个对象所有的属性名和属性值写到文件中去。
1 2 3 4 5 6 7 8 9 10 public class Test5Frame { @Test public void save () throws Exception{ Student s1 = new Student ("黑马吴彦祖" ,45 , '男' , 185.3 , "篮球,冰球,阅读" ); Teacher s2 = new Teacher ("播妞" ,999.9 ); ObjectFrame.save(s1); ObjectFrame.save(s2); } }
打开data.txt文件,内容如下图所示,就说明我们这个框架的功能已经实现了
好了,同学们,恭喜大家!学习到这里,反射技术已经学习完毕了。
注解 认识注解&定义注解 接下来我们学习注解。注解和反射一样,都是用来做框架的,我们这里学习注解的目的其实是为了以后学习框架或者做框架做铺垫的。
那注解该怎么学呢?和反射的学习套路一样,我们先充分的认识注解,掌握注解的定义和使用格式,然后再学习它的应用场景。
先来认识一下什么是注解?
Java注解是代码中的特殊标记,比如@Override、@Test等,作用是:让其他程序根据注解信息决定怎么执行该程序。
比如:Junit框架的@Test注解可以用在方法上,用来标记这个方法是测试方法,被@Test标记的方法能够被Junit框架执行。
再比如:@Override注解可以用在方法上,用来标记这个方法是重写方法,被@Override注解标记的方法能够被IDEA识别进行语法检查。
注解不光可以用在方法上,还可以用在类上、变量上、构造器上等位置。
上面我们说的@Test注解、@Overide注解是别人定义好给我们用的,将来如果需要自己去开发框架,就需要我们自己定义注解。
接着我们学习自定义注解
自定义注解的格式如下图所示
public @interface 注解名称{ public 属性类型属性名() default默认值; }
比如:现在我们自定义一个MyTest注解
1 2 3 4 5 public @interface MyTest{ String aaa () ; boolean bbb () default true ; String[] ccc(); }
定义好MyTest注解之后,我们可以使用MyTest注解在类上、方法上等位置做标记。注意使用注解时需要加@符号,如下
1 2 3 4 5 6 7 @MyTest1(aaa="牛魔王",ccc={"HTML","Java"}) public class AnnotationTest1 { @MyTest(aaa="铁扇公主",bbb=false, ccc={"Python","前端","Java"}) public void test1 () { } }
注意:注解的属性名如何是value的话,并且只有value没有默认值,使用注解时value名称可以省略。比如现在重新定义一个MyTest2注解
1 2 3 4 public @interface MyTest2{ String value () ; int age () default 10 ; }
定义好MyTest2注解后,再将@MyTest2标记在类上,此时value属性名可以省略,代码如下
1 2 3 4 5 6 7 8 @MyTest2("孙悟空") @MyTest1(aaa="牛魔王",ccc={"HTML","Java"}) public class AnnotationTest1 { @MyTest(aaa="铁扇公主",bbb=false, ccc={"Python","前端","Java"}) public void test1 () { } }
到这里关于定义注解的格式、以及使用注解的格式就学习完了。
注解本质是什么呢?
想要搞清楚注解本质是什么东西,我们可以把注解的字节码进行反编译,使用XJad工具进行反编译。经过对MyTest1注解字节码反编译我们会发现:
1 2 3 4 1. MyTest1注解本质上是接口,每一个注解接口都继承子Annotation接口2. MyTest1注解中的属性本质上是抽象方法3. @MyTest1 实际上是作为MyTest接口的实现类对象4. @MyTest1(aaa="孙悟空",bbb=false,ccc={"Python","前端","Java"}) 里面的属性值,可以通过调用aaa()、bbb()、ccc()方法获取到。 【别着急,继续往下看,再解析注解时会用到】
元注解 各位小伙伴,刚才我们已经认识了注解以及注解的基本使用。接下来我们还需要学习几种特殊的注解,叫做元注解。
什么是元注解?
元注解是修饰注解的注解 。这句话虽然有一点饶,但是非常准确。我们看一个例子
接下来分别看一下@Target注解和@Retention注解有什么作用,如下图所示
1 2 @Target 是用来声明注解只能用在那些位置,比如:类上、方法上、成员变量上等@Retetion 是用来声明注解保留周期,比如:源代码时期、字节码时期、运行时期
@Target元注解的使用:比如定义一个MyTest3注解,并添加@Target注解用来声明MyTest3的使用位置
1 2 3 4 @Target(ElementType.TYPE) public @interface MyTest3{ }
接下来,我们把@MyTest3用来类上观察是否有错,再把@MyTest3用在方法上、变量上再观察是否有错
如果我们定义MyTest3注解时,使用@Target注解属性值写成下面样子
1 2 3 4 5 @Target({ElementType.TYPE,ElementType.METHOD}) public @interface MyTest3{ }
此时再观察,@MyTest用在类上、方法上、变量上是否有错
到这里@Target元注解的使用就演示完毕了。
@Retetion元注解的使用:定义MyTest3注解时,给MyTest3注解添加@Retetion注解来声明MyTest3注解保留的时期
1 2 3 4 5 @Retetion 是用来声明注解保留周期,比如:源代码时期、字节码时期、运行时期 @Retetion(RetetionPloicy.SOURCE) : 注解保留到源代码时期、字节码中就没有了 @Retetion(RetetionPloicy.CLASS) : 注解保留到字节码中、运行时注解就没有了 @Retetion(RetetionPloicy.RUNTIME) :注解保留到运行时期 【自己写代码时,比较常用的是保留到运行时期】
1 2 3 4 5 6 7 @Target({ElementType.TYPE,ElementType.METHOD}) @Retetion(RetetionPloicy.RUNTIME) public @interface MyTest3{ }
解析注解 各位小伙伴,通过前面的学习我们能够自己定义注解,也能够把自己定义的注解标记在类上或者方法上等位置,但是总感觉有点别扭,给类、方法、变量等加上注解后,我们也没有干什么呀!!!
接下来,我们就要做点什么。我们可以通过反射技术把类上、方法上、变量上的注解对象获取出来,然后通过调用方法就可以获取注解上的属性值了。我们把获取类上、方法上、变量上等位置注解及注解属性值的过程称为解析注解。
解析注解套路如下
1 2 3 4 1. 如果注解在类上,先获取类的字节码对象,再获取类上的注解2. 如果注解在方法上,先获取方法对象,再获取方法上的注解3. 如果注解在成员变量上,先获取成员变量对象,再获取变量上的注解总之:注解在谁身上,就先获取谁,再用谁获取谁身上的注解
Class、Method ,Field , Constructor、都实现了AnnotatedElement接口,它们都拥有解析注解的能力。
AnnotatedElement接口提供了解析注解的方法 说明 public Annotation[] getDeclaredAnnotations() 获取当前对象上面的注解。 public T getDeclaredAnnotation(class annotationclass) 获取指定的注解对象 public boolean isAnnotationPresent(class annotationclass) 判断当前对象上是否存在某个注解
解析来看一个案例,来演示解析注解的代码编写
按照需求要求一步一步完成
① 先定义一个MyTest4注解
1 2 3 4 5 6 7 8 9 @Target({ElementType.TYPE,ElementType.METHOD}) @Retetion(RetetionPloicy.RUNTIME) public @interface MyTest4{ String value () ; double aaa () default 100 ; String[] bbb(); }
② 定义有一个类Demo
1 2 3 4 5 6 7 @MyTest4(value="蜘蛛侠",aaa=99.9, bbb={"至尊宝","黑马"}) public class Demo { @MyTest4(value="孙悟空",aaa=199.9, bbb={"紫霞","牛夫人"}) public void test1 () { } }
③ 写一个测试类AnnotationTest3解析Demo类上的MyTest4注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public class AnnotationTest3 { @Test public void parseClass () { Class c = Demo.class; if (c.isAnnotationPresent(MyTest4.class)){ MyTest4 myTest4 = (MyTest4)c.getDeclaredAnnotation(MyTest4.class); System.out.println(myTest4.value()); System.out.println(myTest4.aaa()); System.out.println(myTest4.bbb()); } } @Test public void parseMethods () { Class c = Demo.class; Method m = c.getDeclaredMethod("test1" ); if (m.isAnnotationPresent(MyTest4.class)){ MyTest4 myTest4 = (MyTest4)m.getDeclaredAnnotation(MyTest4.class); System.out.println(myTest4.value()); System.out.println(myTest4.aaa()); System.out.println(myTest4.bbb()); } } }
注解的应用场景 各位同学,关于注解的定义、使用、解析注解就已经学习完了。接下来,我们再学习一下注解的应用场景,注解是用来写框架的,比如现在我们要模拟Junit写一个测试框架,要求有@MyTest注解的方法可以被框架执行,没有@MyTest注解的方法不能被框架执行。
第一步:先定义一个MyTest注解
1 2 3 4 5 @Target(ElementType.METHOD) @Retetion(RetetionPloicy.RUNTIME) public @interface MyTest{ }
第二步:写一个测试类AnnotationTest4,在类中定义几个被@MyTest注解标记的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public class AnnotationTest4 { @MyTest public void test1 () { System.out.println("=====test1====" ); } @MyTest public void test2 () { System.out.println("=====test2====" ); } public void test3 () { System.out.println("=====test2====" ); } public static void main (String[] args) { AnnotationTest4 a = new AnnotationTest4 (); Class c = AnnotationTest4.class; Method[] methods = c.getDeclaredMethods(); for (Method m: methods){ if (m.isAnnotationPresent(MyTest.class)){ m.invoke(a); } } } }
学习到这里,关于注解的使用就学会了(^▽^ )
动态代理 动态代理介绍、准备功能 这节课我们学习一个Java的高级技术叫做动态代理。首先我们认识一下代理长什么样?我们以大明星“杨超越”例。
假设现在有一个大明星叫杨超越,它有唱歌和跳舞的本领,作为大明星是要用唱歌和跳舞来赚钱的,但是每次做节目,唱歌的时候要准备话筒、收钱,再唱歌;跳舞的时候也要准备场地、收钱、再唱歌。杨超越越觉得我擅长的做的事情是唱歌,和跳舞,但是每次唱歌和跳舞之前或者之后都要做一些繁琐的事情,有点烦。于是杨超越就找个一个经济公司,请了一个代理人,代理杨超越处理这些事情,如果有人想请杨超越演出,直接找代理人就可以了。如下图所示
我们说杨超越的代理是中介公司派的,那中介公司怎么知道,要派一个有唱歌和跳舞功能的代理呢?
解决这个问题,Java使用的是接口,杨超越想找代理,在Java中需要杨超越实现了一个接口,接口中规定要唱歌和跳舞的方法。Java就可以通过这个接口为杨超越生成一个代理对象,只要接口中有的方法代理对象也会有。
接下来我们就先把有唱歌和跳舞功能的接口,和实现接口的大明星类定义出来。
生成动态代理对象 下面我们写一个为BigStar生成动态代理对象的工具类。这里需要用Java为开发者提供的一个生成代理对象的类叫Proxy类。
通过Proxy类的newInstance(…)方法可以为实现了同一接口的类生成代理对象。 调用方法时需要传递三个参数,该方法的参数解释可以查阅API文档,如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class ProxyUtil { public static Star createProxy (BigStar bigStar) { Star starProxy = (Star) Proxy.newProxyInstance(ProxyUtil.class.getClassLoader(), new Class []{Star.class}, new InvocationHandler () { @Override public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { if (method.getName().equals("sing" )){ System.out.println("准备话筒,收钱20万" ); }else if (method.getName().equals("dance" )){ System.out.println("准备场地,收钱1000万" ); } return method.invoke(bigStar, args); } }); return starProxy; } }
调用我们写好的ProxyUtil工具类,为BigStar对象生成代理对象
1 2 3 4 5 6 7 8 9 10 11 public class Test { public static void main (String[] args) { BigStar s = new BigStar ("杨超越" ); Star starProxy = ProxyUtil.createProxy(s); String rs = starProxy.sing("好日子" ); System.out.println(rs); starProxy.dance(); } }
运行测试类,结果如下图所示
当你把上面的案例写出来,并且理解,那么动态代理的基本使用就学会了。
动态代理应用 学习完动态代理的基本使用之后,接下来我们再做一个应用案例。
现有如下代码
1 2 3 4 5 6 7 8 9 10 11 public interface UserService { void login (String loginName,String passWord) throws Exception; void deleteUsers () throws Exception; String[] selectUsers() throws Exception; }
下面有一个UserService接口的实现类,下面每一个方法中都有计算方法运行时间的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public class UserServiceImpl implements UserService { @Override public void login (String loginName, String passWord) throws Exception { long time1 = System.currentTimeMillis(); if ("admin" .equals(loginName) && "123456" .equals(passWord)){ System.out.println("您登录成功,欢迎光临本系统~" ); }else { System.out.println("您登录失败,用户名或密码错误~" ); } Thread.sleep(1000 ); long time2 = System.currentTimeMillis(); System.out.println("login方法耗时:" +(time2-time1)); } @Override public void deleteUsers () throws Exception{ long time1 = System.currentTimeMillis(); System.out.println("成功删除了1万个用户~" ); Thread.sleep(1500 ); long time2 = System.currentTimeMillis(); System.out.println("deleteUsers方法耗时:" +(time2-time1)); } @Override public String[] selectUsers() throws Exception{ long time1 = System.currentTimeMillis(); System.out.println("查询出了3个用户" ); String[] names = {"张全蛋" , "李二狗" , "牛爱花" }; Thread.sleep(500 ); long time2 = System.currentTimeMillis(); System.out.println("selectUsers方法耗时:" +(time2-time1)); return names; } }
观察上面代码发现有什么问题吗?
我们会发现每一个方法中计算耗时的代码都是重复的,我们可是学习了动态代理的高级程序员,怎么能忍受在每个方法中写重复代码呢!况且这些重复的代码并不属于UserSerivce的主要业务代码。
所以接下来我们打算,把计算每一个方法的耗时操作,交给代理对象来做。
先在UserService类中把计算耗时的代码删除,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class UserServiceImpl implements UserService { @Override public void login (String loginName, String passWord) throws Exception { if ("admin" .equals(loginName) && "123456" .equals(passWord)){ System.out.println("您登录成功,欢迎光临本系统~" ); }else { System.out.println("您登录失败,用户名或密码错误~" ); } Thread.sleep(1000 ); } @Override public void deleteUsers () throws Exception{ System.out.println("成功删除了1万个用户~" ); Thread.sleep(1500 ); } @Override public String[] selectUsers() throws Exception{ System.out.println("查询出了3个用户" ); String[] names = {"张全蛋" , "李二狗" , "牛爱花" }; Thread.sleep(500 ); return names; } }
然后为UserService生成一个动态代理对象,在动态代理中调用目标方法,在调用目标方法之前和之后记录毫秒值,并计算方法运行的时间。代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class ProxyUtil { public static UserService createProxy (UserService userService) { UserService userServiceProxy = (UserService) Proxy.newProxyInstance( ProxyUtil.class.getClassLoader(), new Class []{UserService.class}, new InvocationHandler () { @Override public Object invoke ( Object proxy, Method method, Object[] args) throws Throwable { if ( method.getName().equals("login" ) || method.getName().equals("deleteUsers" )|| method.getName().equals("selectUsers" )){ long startTime = System.currentTimeMillis(); Object rs = method.invoke(userService, args); long endTime = System.currentTimeMillis(); System.out.println(method.getName() + "方法执行耗时:" + (endTime - startTime)/ 1000.0 + "s" ); return rs; }else { Object rs = method.invoke(userService, args); return rs; } } }); return userServiceProxy; } }
在测试类中为UserService创建代理对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class Test { public static void main (String[] args) throws Exception{ UserService userService = ProxyUtil.createProxy(new UserServiceImpl ()); userService.login("admin" , "123456" ); System.out.println("----------------------------------" ); userService.deleteUsers(); System.out.println("----------------------------------" ); String[] names = userService.selectUsers(); System.out.println("查询到的用户是:" + Arrays.toString(names)); System.out.println("----------------------------------" ); } }
执行结果如下图所示
动态代理对象的执行流程如下图所示,每次用代理对象调用方法时,都会执行InvocationHandler中的invoke方法。
动态代理我们已经学习完了。到此整个JavaSE的课程也学习完了。