在此感谢黑马程序员的视频教程 !
面向对象高级(一)
一、静态
1.1 static修饰成员变量
Java中的成员变量按照有无static修饰分为两种:类变量、实例变量 。它们的区别如下图所示:
由于静态变量是属于类的,只需要通过类名就可以调用:类名.静态变量
实例变量是属于对象的,需要通过对象才能调用:对象.实例变量
1 2 - 1. 类变量:属于类,在内存中只有一份,用类名调用 - 2. 实例变量:属于对象,每一个对象都有一份,用对象调用
1.2 static修饰成员变量的应用场景
在实际开发中,如果某个数据只需要一份,且希望能够被共享(访问、修改),则该数据可以定义成员变量来记住。
我们看一个案例
需求:系统启动后,要求用于类可以记住自己创建了多少个用户对象。**
第一步:先定义一个User
类,在用户类中定义一个static修饰的变量,用来表示在线人数;
1 2 3 4 5 6 7 public class User { public static int number; public User () { User.number++; } }
第二步:再写一个测试类,再测试类中创建4个User对象,再打印number的值,观察number的值是否再自增。
1 2 3 4 5 6 7 8 9 10 11 12 public class Test { public static void main (String[] args) { new User (); new User (); new User (); new User (); System.out.println("系统创建的User对象个数:" +User.number); } }
运行上面的代码,查看执行结果是:系统创建的User对象个数:4
1.3 static修饰成员方法
成员方法根据有无static也分为两类:类方法、实例方法
有static修饰的方法,是属于类的,称为类方法 ;调用时直接用类名调用即可。
无static修饰的方法,是属于对象的,称为实例方法;调用时,需要使用对象调用。
我们看一个案例,演示类方法、实例方法的基本使用
先定义一个Student类,在类中定义一个类方法、定义一个实例方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class Student { double score; public static void printHelloWorld{ System.out.println("Hello World!" ); System.out.println("Hello World!" ); } public void printPass () { System.out.println(score>=60 ?"成绩合格" :"成绩不合格" ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Test2 { public static void main (String[] args) { Student.printHelloWorld(); Student s = new Student (); s.printPass(); s.printHelloWorld(); } }
1 2 3 4 1 .类方法:static修饰的方法,可以被类名调用,是因为它是随着类的加载而加载的; 所以类名直接就可以找到static修饰的方法 2 .实例方法:非static修饰的方法,需要创建对象后才能调用,是因为实例方法中可能会访问实例变量,而实例变量需要创建对象后才存在。所以实例方法,必须创建对象后才能调用。
1.4 工具类
如果一个类中的方法全都是静态的,那么这个类中的方法就全都可以被类名直接调用,由于调用起来非常方便,就像一个工具一下,所以把这样的类就叫做工具类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class MyUtils { public static String createCode (int n) { String code = "" ; String data = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKMNOPQRSTUVWXYZ" ; Random r = new Random (); for (int i=0 ; i<n; i++){ int index = r.nextInt(data.length()); char ch = data.charAt(index); code+=ch; } return code; } }
接着可以在任何位置调用MyUtils
的createCOde()方法
产生任意个数的验证码
1 2 3 4 5 6 public class LoginDemo { public static void main (String[] args) { System.out.println(MyUtils.createCode()); } }
1 2 3 4 5 6 public class registerDemo { public static void main (String[] args) { System.out.println(MyUtils.createCode()); } }
在补充一点,工具类里的方法全都是静态的,推荐用类名调用为了防止使用者用对象调用。我们可以把工具类的构造方法私有化。
1 2 3 4 5 6 7 8 9 10 11 public class MyUtils { private MyUtils () { } public static String createCode (int n) { ... } }
1.5 static的注意事项
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 Student { static String schoolName; double score; public static void printHelloWorld () { schoolName = "学校" ; printHelloWorld2(); System.out.println(score); printPass(); ystem.out.println(this ); } public static void printHelloWorld2 () { } public void printPass2 () { } public void printPass () { schoolName = "学校2" ; printHelloWorld2(); System.out.println(score); printPass2(); System.out.println(this ); } }
1.6 static应用(代码块)
代码块根据有无static修饰分为两种:静态代码块、实例代码块
静态代码块:
1 2 3 4 5 6 7 8 9 public class Student { static int number = 80 ; static String schoolName = "黑马" ; static { System.out.println("静态代码块执行了~~" ); schoolName = "黑马" ; } }
静态代码块不需要创建对象就能够执行
1 2 3 4 5 6 7 8 9 10 public class Test { public static void main (String[] args) { System.out.println(Student.number); System.out.println(Student.number); System.out.println(Student.number); System.out.println(Student.schoolName); } }
执行上面代码时,发现没有创建对象,静态代码块就已经执行了。
关于静态代码块重点注意:静态代码块,随着类的加载而执行,而且只执行一次。
实例代码块
实例代码块的作用和构造器的作用是一样的,用来给对象初始化值;而且每次创建对象之前都会先执行实例代码块。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class Student { int age; { System.out.println("实例代码块执行了~~" ); age = 18 ; System.out.println("有人创建了对象:" + this ); } public Student () { System.out.println("无参数构造器执行了~~" ); } public Student (String name) { System.out.println("有参数构造器执行了~~" ); } }
接下来在测试类中进行测试,观察创建对象时,实例代码块是否先执行了。
1 2 3 4 5 6 7 8 public class Test { public static void main (String[] args) { Student s1 = new Student (); Student s2 = new Student ("张三" ); System.out.println(s1.age); System.out.println(s2.age); } }
对于实例代码块重点注意:实例代码块每次创建对象之前都会执行一次
二、继承
2.1 继承
接下来,我们演示一下使用继承来编写代码,注意观察继承的特点。
1 2 3 4 5 6 7 8 9 10 11 12 13 public class A { public int i; public void print1 () { System.out.println("===print1===" ); } private int j; private void print2 () { System.out.println("===print2===" ); } }
然后,写一个B类,让B类继承A类。在继承A类的同时,B类中新增一个方法print3
1 2 3 4 5 6 7 8 9 10 11 public class B extends A { public void print3 () { System.out.println(i); print1(); System.out.println(j); print2(); } }
这里我们只需要关注一点:子类对象实际上是由子、父类两张设计图共同创建出来的。
所以,在子类对象的空间中,既有本类的成员,也有父类的成员。但是子类只能调用父类公有的成员。
2.2 继承的好处
观察代码发现,我们会发现Teacher类中和Consultant类中有相同的代码;其实像这种两个类中有相同代码时,没必要重复写。
我们可以把重复的代码提取出来,作为父类,然后让其他类继承父类就可以了,这样可以提高代码的复用性。改造后的代码如下:
接下来使用继承来完成上面的案例,这里只演示People类和Teacher类,然后你尝试自己完成Consultant类。
先写一个父类 People,用来设计Teacher和Consultant公有的成员。
1 2 3 4 5 6 7 8 9 10 public class People { private String name; public String getName () { return name; } public void setName (String name) { this .name=name; } }
再写两个子类Teacher继承People类,同时在子类中加上自己特有的成员。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class Teacher extends People { private String skill; public String getSkill () { return skill; } public void setSkill (String skill) { this .skill=skill; } public void printInfo () { System.out.println(getName()+"具备的技能:" +skill); } }
最后再写一个测试类,再测试类中创建Teacher、Consultant对象,并调用方法。
1 2 3 4 5 6 7 8 9 10 11 public class Test { public static void main (String[] args) { Teacher t = new Teacher (); t.setName("播仔" ); t.setSkill("Java、Spring" ); System.out.println(t.getName()); System.out.println(t.getSkill()); t.printInfo(); } }
执行代码,打印结果如下:
关于继承的好处我们只需要记住:继承可以提高代码的复用性
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 public class Fu { private void privateMethod () { System.out.println("==private==" ); } void method () { System.out.println("==缺省==" ); } protected void protectedMethod () { System.out.println("==protected==" ); } public void publicMethod () { System.out.println("==public==" ); } public void test () { privateMethod(); method(); protectedMethod(); publicMethod(); } }
接下来,在和Fu类同一个包下,创建一个测试类Demo,演示同一个包下可以访问到哪些权限修饰的方法。
1 2 3 4 5 6 7 8 9 public class Demo { public static void main (String[] args) { Fu f = new Fu (); f.method(); f.protectedMethod(); f.publicMethod(); } }
接下来,在另一个包下创建一个Fu类的子类,演示不同包下的子类中可以访问哪些权限修饰的方法。
1 2 3 4 5 6 7 8 9 public class Zi extends Fu { public void test () { protectedMethod(); publicMethod(); } }
接下来,在和Fu类不同的包下,创建一个测试类Demo2,演示一下不同包的无关类,能访问到哪些权限修饰的方法;
1 2 3 4 5 6 7 8 9 10 11 12 public class Demo2 { public static void main (String[] args) { Fu f = new Fu (); f.publicMethod(); Zi zi = new Zi (); } }
2.4 单继承、Object
Java语言只支持单继承,不支持多继承,但是可以多层继承 。就像家族里儿子、爸爸和爷爷的关系一样:一个儿子只能有一个爸爸,不能有多个爸爸,但是爸爸也是有爸爸的。
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) { A a = new A (); B b = new B (); ArrayList list = new ArrayList (); list.add("java" ); System.out.println(list.toString()); } } class A {} class B extends A {}class D extends B {}
2.5 方法重写
什么是方法重写
当子类觉得父类方法不好用,或者无法满足父类需求时,子类可以重写一个方法名称、参数列表一样的方法,去覆盖父类的这个方法,这就是方法重写。
注意:重写后,方法的访问遵循就近原则 。下面我们看一个代码演示
写一个A类作为父类,定义两个方法print1和print2
1 2 3 4 5 6 7 8 9 public class A { public void print1 () { System.out.println("111" ); } public void print2 (int a, int b) { System.out.println("111111" ); } }
再写一个B类作为A类的子类,重写print1和print2方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class B extends A { @Override public void print1 () { System.out.println("666" ); } @Override public void print2 (int a, int b) { System.out.println("666666" ); } }
接下来,在测试类中创建B类对象,调用方法
1 2 3 4 5 6 7 8 public class Test { public static void main (String[] args) { B b = new B (); b.print1(); b.print2(2 , 3 ); } }
执行代码,我们发现真正执行的是B类中的print1和print2方法
知道什么是方法重写之后,还有一些注意事项,需要和大家分享一下。
1 2 3 4 5 - 1. 重写的方法上面,可以加一个注解@Override ,用于标注这个方法是复写的父类方法 - 2. 子类复写父类方法时,访问权限必须大于或者等于父类方法的权限 public > protected > 缺省 - 3. 重写的方法返回值类型,必须与被重写的方法返回值类型一样,或者范围更小 - 4. 私有方法、静态方法不能被重写,如果重写会报错。
关于这些注意事项,同学们其实只需要了解一下就可以了。实际上我们实际写代码时,只要和父类写的一样就可以( 总结起来就8个字:声明不变,重新实现 )
方法重写的应用场景
方法重写的应用场景之一就是:子类重写Object的toString()方法,以便返回对象的内容。
比如:有一个Student类,这个类会默认继承Object类。
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 extends Object { private String name; private int age; public Student () { } public Student (String name, int age) { this .name = name; this .age = age; } public String getName () { return name; } public void setName (String name) { this .name = name; } public int getAge () { return age; } public void setAge (int age) { this .age = age; } }
其实Object类中有一个toString()方法,直接通过Student对象调用Object的toString()方法,会得到对象的地址值。
1 2 3 4 5 6 7 public class Test { public static void main (String[] args) { Student s = new Student ("播妞" , 19 ); System.out.println(s); } }
但是,此时不想调用父类Object的toString()方法,那就可以在Student类中重新写一个toSting()方法,用于返回对象的属性值。
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 package com.itheima.d12_extends_override;public class Student extends Object { private String name; private int age; public Student () { } public Student (String name, int age) { this .name = name; this .age = age; } public String getName () { return name; } public void setName (String name) { this .name = name; } public int getAge () { return age; } public void setAge (int age) { this .age = age; } @Override public String toString () { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}' ; } }
重新运行测试类,结果如下
2.6 子类中访问成员的特点
原则:在子类中访问其他成员(成员变量、成员方法),是依据就近原则的
定义一个父类,代码如下
1 2 3 4 5 6 7 public class F { String name = "父类名字" ; public void print1 () { System.out.println("==父类的print1方法执行==" ); } }
再定义一个子类,代码如下。有一个同名的name成员变量,有一个同名的print1成员方法;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class Z extends F { String name = "子类名称" ; public void showName () { String name = "局部名称" ; System.out.println(name); } @Override public void print1 () { System.out.println("==子类的print1方法执行了=" ); } public void showMethod () { print1(); } }
接下来写一个测试类,观察运行结果,我们发现都是调用的子类变量、子类方法。
1 2 3 4 5 6 7 8 public class Test { public static void main (String[] args) { Z z = new Z (); z.showName(); z.showMethod(); } }
如果子类和父类出现同名变量或者方法,优先使用子类的;此时如果一定要在子类中使用父类的成员,可以加this或者super进行区分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class Z extends F { String name = "子类名称" ; public void showName () { String name = "局部名称" ; System.out.println(name); System.out.println(this .name); System.out.println(super .name); } @Override public void print1 () { System.out.println("==子类的print1方法执行了=" ); } public void showMethod () { print1(); super .print1(); } }
2.7 子类中访问构造器的特点
我们先认识子类构造器的语法特点,再讲一下子类构造器的应用场景
子类中访问构造器的语法规则
子类访问构造器的应用场景
如果不想使用默认的super()
方式调用父类构造器,还可以手动使用super(参数)
调用父类有参数构造器。
在本类中访问自己的构造方法
刚才我们学习了通过super()
和super(参数)
可以访问父类的构造器。有时候我们也需要访问自己类的构造器。语法如下
1 2 this (): 调用本类的空参数构造器this (参数): 调用本类有参数的构造器
最后我们被this和super的用法在总结一下
1 2 3 4 5 6 7 8 9 10 11 12 13 访问本类成员: this .成员变量 this .成员方法 this () this (参数) 访问父类成员: super .成员变量 super .成员方法 super () super (参数) 注意:this 和super 访问构造方法,只能用到构造方法的第一句,否则会报错。
面向对象高级(二)
一、多态
1.1 多态概述
什么是多态?
多态是在继承、实现情况下的一种现象,表现为:对象多态、行为多态。
比如:Teacher和Student都是People的子类,代码可以写成下面的样子
1.2 多态的好处
在多态形式下,右边的代码是解耦合的,更便于扩展和维护。
怎么理解这句话呢?比如刚开始p1指向Student对象,run方法执行的就是Student对象的业务;假如p1指向Student对象 ,run方法执行的自然是Student对象的业务。
定义方法时,使用父类类型作为形参,可以接收一切子类对象,扩展更强,更便利。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class Test2 { public static void main (String[] args) { Teacher t = new Teacher (); go(t); Student s = new Student (); go(s); } public static void go (People p) { System.out.println("开始------------------------" ); p.run(); System.out.println("结束------------------------" ); } }
1.3 类型转换
虽然多态形式下有一些好处,但是也有一些弊端。在多态形式下,不能调用子类特有的方法,比如在Teacher类中多了一个teach方法,在Student类中多了一个study方法,这两个方法在多态形式下是不能直接调用的。
多态形式下不能直接调用子类特有方法,但是转型后是可以调用的。这里所说的转型就是把父类变量转换为子类类型。格式如下:
1 2 3 4 5 if (父类变量 instance 子类){ 子类 变量名 = (子类)父类变量; }
如果类型转换错了,就会出现类型转换异常ClassCastException,比如把Teacher类型转换成了Student类型.
关于多态转型问题,我们最终记住一句话:原本是什么类型,才能还原成什么类型
二、final关键字
2.1 final修饰符的特点
我们先来认识一下final的特点,final关键字是最终的意思,可以修饰类、修饰方法、修饰变量。
1 2 3 - final 修饰类:该类称为最终类,特点是不能被继承 - final 修饰方法:该方法称之为最终方法,特点是不能被重写。 - final 修饰变量:该变量只能被赋值一次。
接下来我们分别演示一下,先看final修饰类的特点
再来演示一下final修饰方法的特点
再演示一下final修饰变量的特点
2.2 补充知识:常量
被 static final 修饰的成员变量,称之为常量。
通常用于记录系统的配置信息
接下来我们用代码来演示一下
1 2 3 4 5 6 public class Constant { public static final String SCHOOL_NAME = "传智教育" ; }
1 2 3 4 5 6 7 8 9 10 11 12 public class FinalDemo2 { public static void main (String[] args) { System.out.println(Constant.SCHOOL_NAME); System.out.println(Constant.SCHOOL_NAME); System.out.println(Constant.SCHOOL_NAME); System.out.println(Constant.SCHOOL_NAME); System.out.println(Constant.SCHOOL_NAME); System.out.println(Constant.SCHOOL_NAME); System.out.println(Constant.SCHOOL_NAME); } }
关于常量的原理,同学们也可以了解一下:在程序编译后,常量会“宏替换”,出现常量的地方,全都会被替换为其记住的字面量。把代码反编译后,其实代码是下面的样子
1 2 3 4 5 6 7 8 9 10 11 public class FinalDemo2 { public static void main (String[] args) { System.out.println("传智教育" ); System.out.println("传智教育" E); System.out.println("传智教育" ); System.out.println("传智教育" ); System.out.println("传智教育" ); System.out.println("传智教育" ); System.out.println("传智教育" ); } }
三、抽象
3.1 认识抽象类
我们先来认识一下什么是抽象类,以及抽象类有什么特点。
在Java中有一个关键字叫abstract,它就是抽象的意思,它可以修饰类也可以修饰方法。
1 2 - 被abstract 修饰的类,就是抽象类 - 被abstract 修饰的方法,就是抽象方法(不允许有方法体)
接下来用代码来演示一下抽象类和抽象方法
1 2 3 4 5 public abstract class A { public abstract void test () ; }
类的成员(成员变量、成员方法、构造器),类的成员都可以有。如下面代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public abstract class A { private String name; static String schoolName; public A () { } public abstract void test () ; public String getName () { return name; } public void setName (String name) { this .name = name; } }
抽象类虽然不能创建对象,但是它可以作为父类让子类继承。而且子类继承父类必须重写父类的所有抽象方法。
1 2 3 4 5 6 7 public class B extends A { @Override public void test () { } }
子类继承父类如果不复写父类的抽象方法,要想不出错,这个子类也必须是抽象类
1 2 3 4 public abstract class B extends A {}
3.2 抽象类的好处
分析需求发现,该案例中猫和狗都有名字这个属性,也都有叫这个行为,所以我们可以将共性的内容抽取成一个父类,Animal类,但是由于猫和狗叫的声音不一样,于是我们在Animal类中将叫的行为写成抽象的。代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public abstract class Animal { private String name; public abstract void cry () ; public String getName () { return name; } public void setName (String name) { this .name = name; } }
接着写一个Animal的子类,Dog类。代码如下
1 2 3 4 5 public class Dog extends Animal { public void cry () { System.out.println(getName() + "汪汪汪的叫~~" ); } }
然后,再写一个Animal的子类,Cat类。代码如下
1 2 3 4 5 public class Cat extends Animal { public void cry () { System.out.println(getName() + "喵喵喵的叫~~" ); } }
最后,再写一个测试类,Test类。
1 2 3 4 5 6 7 public class Test2 { public static void main (String[] args) { Animal a = new Dog (); a.cry(); } }
再学一招,假设现在系统有需要加一个Pig类,也有叫的行为,这时候也很容易原有功能扩展。只需要让Pig类继承Animal,复写cry方法就行。
1 2 3 4 5 6 public class Pig extends Animal { @Override public void cry () { System.out.println(getName() + "嚯嚯嚯~~~" ); } }
此时,创建对象时,让Animal接收Pig,就可以执行Pig的cry方法
1 2 3 4 5 6 7 public class Test2 { public static void main (String[] args) { Animal a = new Pig (); a.cry(); } }
综上所述,我们总结一下抽象类的使用场景和好处
1 2 3 1. 用抽象类可以把父类中相同的代码,包括方法声明都抽取到父类,这样能更好的支持多态,一提高代码的灵活性。2. 反过来用,我们不知道系统未来具体的业务实现时,我们可以先定义抽象类,将来让子类去实现,以方便系统的扩展。
3.3 模板方法模式
那模板方法设计模式解决什么问题呢?模板方法模式主要解决方法中存在重复代码的问题
比如A类和B类都有sing()方法,sing()方法的开头和结尾都是一样的,只是中间一段内容不一样。此时A类和B类的sing()方法中就存在一些相同的代码。
怎么解决上面的重复代码问题呢? 我们可以写一个抽象类C类,在C类中写一个doSing()的抽象方法。再写一个sing()方法,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 public abstract class C { public final void sing () { System.out.println("唱一首你喜欢的歌:" ); doSing(); System.out.println("唱完了!" ); } public abstract void doSing () ; }
然后,写一个A类继承C类,复写doSing()方法,代码如下
1 2 3 4 5 6 public class A extends C { @Override public void doSing () { System.out.println("我是一只小小小小鸟,想要飞就能飞的高~~~" ); } }
接着,再写一个B类继承C类,也复写doSing()方法,代码如下
1 2 3 4 5 6 public class B extends C { @Override public void doSing () { System.out.println("我们一起学猫叫,喵喵喵喵喵喵喵~~" ); } }
最后,再写一个测试类Test
1 2 3 4 5 6 7 public class Test { public static void main (String[] args) { B b = new B (); b.sing(); } }
综上所述:模板方法模式解决了多个子类中有相同代码的问题。具体实现步骤如下
1 2 3 第1 步:定义一个抽象类,把子类中相同的代码写成一个模板方法。 第2 步:把模板方法中不能确定的代码写成抽象方法,并在模板方法中调用。 第3 步:子类继承抽象类,只需要父类抽象方法就可以了。
四、接口
4.1 认识接口
我们先来认识一下接口?Java提供了一个关键字interface,用这个关键字来定义接口这种特殊结构。格式如下
1 2 3 4 public interface 接口名{ }
按照接口的格式,我们定义一个接口看看
1 2 3 4 5 6 7 public interface A { public static final String SCHOOL_NAME = "黑马程序员" ; public abstract void test () ; }
写好A接口之后,在写一个测试类,用一下
1 2 3 4 5 6 7 8 9 public class Test { public static void main (String[] args) { System.out.println(A.SCHOOL_NAME); A a = new A (); } }
我们发现定义好接口之后,是不能创建对象的。那接口到底什么使用呢?需要我注意下面两点
接口是用来被类实现(implements)的,我们称之为实现类。
一个类是可以实现多个接口的(接口可以理解成干爹),类实现接口必须重写所有接口的全部抽象方法,否则这个类也必须是抽象类
比如,再定义一个B接口,里面有两个方法testb1(),testb2()
1 2 3 4 public interface B { void testb1 () ; void testb2 () ; }
接着,再定义一个C接口,里面有两个方法testc1(), testc2()
1 2 3 4 public interface C { void testc1 () ; void testc2 () ; }
然后,再写一个实现类D,同时实现B接口和C接口,此时就需要复写四个方法,如下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class D implements B , C{ @Override public void testb1 () { } @Override public void testb2 () { } @Override public void testc1 () { } @Override public void testc2 () { } }
最后,定义一个测试类Test
1 2 3 4 5 6 7 8 9 public class Test { public static void main (String[] args) { System.out.println(A.SCHOOL_NAME); D d = new D (); } }
4.2 接口的好处
使用接口到底有什么好处呢?主要有下面的两点
弥补了类单继承的不足,一个类同时可以实现多个接口。
让程序可以面向接口编程,这样程序员可以灵活方便的切换各种业务实现。
我们看一个案例演示,假设有一个Studnet学生类,还有一个Driver司机的接口,还有一个Singer歌手的接口。
现在要写一个A类,想让他既是学生,偶然也是司机能够开车,偶尔也是歌手能够唱歌。那我们代码就可以这样设计,如下:
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 class Student {} interface Driver { void drive () ; } interface Singer { void sing () ; } class A extends Student implements Driver , Singer{ @Override public void drive () { } @Override public void sing () { } } public class Test { public static void main (String[] args) { Singer s = new A (); s.sing(); Driver d = new A (); d.drive(); } }
综上所述:接口弥补了单继承的不足,同时可以轻松实现在多种业务场景之间的切换。
4.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 public class Student { private String name; private char sex; private double score; public Student () { } public Student (String name, char sex, double score) { this .name = name; this .sex = sex; this .score = score; } public String getName () { return name; } public void setName (String name) { this .name = name; } public char getSex () { return sex; } public void setSex (char sex) { this .sex = sex; } public double getScore () { return score; } public void setScore (double score) { this .score = score; } }
接着,写一个StudentOperator接口,表示学生信息管理系统的两个功能。
1 2 3 4 public interface StudentOperator { void printAllInfo (ArrayList<Student> students) ; void printAverageScore (ArrayList<Student> students) ; }
然后,写一个StudentOperator接口的实现类StudentOperatorImpl1,采用第1套方案对业务进行实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class StudentOperatorImpl1 implements StudentOperator { @Override public void printAllInfo (ArrayList<Student> students) { System.out.println("----------全班全部学生信息如下--------------" ); for (int i = 0 ; i < students.size(); i++) { Student s = students.get(i); System.out.println("姓名:" + s.getName() + ", 性别:" + s.getSex() + ", 成绩:" + s.getScore()); } System.out.println("-----------------------------------------" ); } @Override public void printAverageScore (ArrayList<Student> students) { double allScore = 0.0 ; for (int i = 0 ; i < students.size(); i++) { Student s = students.get(i); allScore += s.getScore(); } System.out.println("平均分:" + (allScore) / students.size()); } }
接着,再写一个StudentOperator接口的实现类StudentOperatorImpl2,采用第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 public class StudentOperatorImpl2 implements StudentOperator { @Override public void printAllInfo (ArrayList<Student> students) { System.out.println("----------全班全部学生信息如下--------------" ); int count1 = 0 ; int count2 = 0 ; for (int i = 0 ; i < students.size(); i++) { Student s = students.get(i); System.out.println("姓名:" + s.getName() + ", 性别:" + s.getSex() + ", 成绩:" + s.getScore()); if (s.getSex() == '男' ){ count1++; }else { count2 ++; } } System.out.println("男生人数是:" + count1 + ", 女士人数是:" + count2); System.out.println("班级总人数是:" + students.size()); System.out.println("-----------------------------------------" ); } @Override public void printAverageScore (ArrayList<Student> students) { double allScore = 0.0 ; double max = students.get(0 ).getScore(); double min = students.get(0 ).getScore(); for (int i = 0 ; i < students.size(); i++) { Student s = students.get(i); if (s.getScore() > max) max = s.getScore(); if (s.getScore() < min) min = s.getScore(); allScore += s.getScore(); } System.out.println("学生的最高分是:" + max); System.out.println("学生的最低分是:" + min); System.out.println("平均分:" + (allScore - max - min) / (students.size() - 2 )); } }
再写一个班级管理类ClassManager,在班级管理类中使用StudentOperator的实现类StudentOperatorImpl1对学生进行操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class ClassManager { private ArrayList<Student> students = new ArrayList <>(); private StudentOperator studentOperator = new StudentOperatorImpl1 (); public ClassManager () { students.add(new Student ("迪丽热巴" , '女' , 99 )); students.add(new Student ("古力娜扎" , '女' , 100 )); students.add(new Student ("马尔扎哈" , '男' , 80 )); students.add(new Student ("卡尔扎巴" , '男' , 60 )); } public void printInfo () { studentOperator.printAllInfo(students); } public void printScore () { studentOperator.printAverageScore(students); } }
最后,再写一个测试类Test,在测试类中使用ClassMananger完成班级学生信息的管理。
1 2 3 4 5 6 7 8 public class Test { public static void main (String[] args) { ClassManager clazz = new ClassManager (); clazz.printInfo(); clazz.printScore(); } }
注意:如果想切换班级管理系统的业务功能,随时可以将StudentOperatorImpl1切换为StudentOperatorImpl2。自己试试
4.4 接口JDK8的新特性
我们看一下这三种方法分别有什么特点?
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 interface A { default void test1 () { System.out.println("===默认方法==" ); test2(); } private void test2 () { System.out.println("===私有方法==" ); } static void test3 () { System.out.println("==静态方法==" ); } void test4 () ; void test5 () ; default void test6 () { } }
接下来我们写一个B类,实现A接口。B类作为A接口的实现类,只需要重写抽象方法就尅了,对于默认方法不需要子类重写。代码如下:
1 2 3 4 5 6 7 8 9 10 11 public class B implements A { @Override public void test4 () { } @Override public void test5 () { } }
最后,写一个测试类,观察接口中的三种方法,是如何调用的
1 2 3 4 5 6 7 8 9 public class Test { public static void main (String[] args) { B b = new B (); b.test1(); A.test3(); } }
综上所述:JDK8对接口新增的特性,有利于对程序进行扩展。
4.5 接口的其他细节
注意事项:
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 Test { public static void main (String[] args) { } } interface A { void test1 () ; } interface B { void test2 () ; } interface C {}interface D extends C , B, A{} class E implements D { @Override public void test1 () { } @Override public void test2 () { } }
接口除了上面的多继承特点之外,在多实现、继承和实现并存时,有可能出现方法名冲突的问题,需要了解怎么解决(仅仅只是了解一下,实际上工作中几乎不会出现这种情况)
1 2 3 4 1. 一个接口继承多个接口,如果多个接口中存在相同的方法声明,则此时不支持多继承2. 一个类实现多个接口,如果多个接口中存在相同的方法声明,则此时不支持多实现3. 一个类继承了父类,又同时实现了接口,父类中和接口中有同名的默认方法,实现类会有限使用父类的方法4. 一个类实现类多个接口,多个接口中有同名的默认方法,则这个类必须重写该方法。
综上所述:一个接口可以继承多个接口,接口同时也可以被类实现。
面向对象高级(三)
一、内部类
内部类是类中的五大成分之一(成员变量、方法、构造器、内部类、代码块),如果一个类定义在另一个类的内部,这个类就是内部类。
当一个类的内部,包含一个完整的事物,且这个事物没有必要单独设计时,就可以把这个事物设计成内部类。
比如:汽车、的内部有发动机,发动机是包含在汽车内部的一个完整事物,可以把发动机设计成内部类。
1 2 3 4 5 6 public class Car { public class Engine { } }
内部类有四种形式,分别是成员内部类、静态内部类、局部内部类、匿名内部类。
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 35 36 37 public class Outer { private int age = 99 ; public static String a="黑马" ; public class Inner { private String name; private int age = 88 ; public void test () { System.out.println(age); System.out.println(a); int age = 77 ; System.out.println(age); System.out.println(this .age); System.out.println(Outer.this .age); } public String getName () { return name; } public void setName (String name) { this .name = name; } public int getAge () { return age; } public void setAge (int age) { this .age = age; } } }
成员内部类如何创建对象,格式如下
1 2 3 4 Outer.Inner in = new Outer ().new Inner (); in.test();
总结一下内部类访问成员的特点
既可以访问内部类成员、也可以访问外部类成员
如果内部类成员和外部类成员同名,可以使用**类名.this.成员
**区分
1.2 静态内部类
静态内部类,其实就是在成员内部类的前面加了一个static关键字。静态内部类属于外部类自己持有。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Outer { private int age = 99 ; public static String schoolName="黑马" ; public static class Inner { public void test () { System.out.println(schoolName); } } }
静态内部类创建对象时,需要使用外部类的类名调用。
1 2 3 Outer.Inner in = new Outer .Inner(); in.test();
1.3 局部内部类
局部内部类是定义在方法中的类,和局部变量一样,只能在方法中有效。所以局部内部类的局限性很强,一般在开发中是不会使用的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Outer { public void test () { class Inner { public void show () { System.out.println("Inner...show" ); } } Inner in = new Inner (); in.show(); } }
1.4 匿名内部类*
1.4.1 认识匿名内部类,基本使用
一种在实际开发中用得最多的一种内部类,叫匿名内部类。相比于前面几种内部类,匿名内部类就比较重要的。
我们还是先认识一下什么是匿名内部类?
匿名内部类是一种特殊的局部内部类;所谓匿名,指的是程序员不需要为这个类声明名字。
下面就是匿名内部类的格式:
1 2 3 4 new 父类/接口(参数值){ @Override 重写父类/接口的方法; }
匿名内部类本质上是一个没有名字的子类对象、或者接口的实现类对象。
比如,先定义一个Animal抽象类,里面定义一个cry()方法,表示所有的动物有叫的行为,但是因为动物还不具体,cry()这个行为并不能具体化,所以写成抽象方法。
1 2 3 public abstract class Animal { public abstract void cry () ; }
接下来,我想要在不定义子类的情况下创建Animal的子类对象,就可以使用匿名内部类
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Test { public static void main (String[] args) { Animal a = new Animal (){ @Override public void cry () { System.out.println("猫喵喵喵的叫~~~" ); } } a.eat(); } }
需要注意的是,匿名内部类在编写代码时没有名字,编译后系统会为自动为匿名内部类生产字节码,字节码的名称会以外部类$1.class
的方法命名
匿名内部类的作用:简化了创建子类对象、实现类对象的书写格式。
1.4.2 匿名内部类的应用场景
一般我们会主动的使用匿名内部类。
**只有在调用方法时,当方法的形参是一个接口或者抽象类,为了简化代码书写,而直接传递匿名内部类对象给方法。**这样就可以少写一个类。比如,看下面代码
1 2 3 public interface Swimming { public void swim () ; }
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 Test { public static void main (String[] args) { Swimming s1 = new Swimming (){ public void swim () { System.out.println("狗刨飞快" ); } }; go(s1); Swimming s1 = new Swimming (){ public void swim () { System.out.println("猴子游泳也还行" ); } }; go(s1); } public static void go (Swimming s) { System.out.println("开始~~~~~~~~" ); s.swim(); System.out.println("结束~~~~~~~~" ); } }
二、枚举
2.1 认识枚举
2.1.1 认识枚举、枚举的原理
枚举是一种特殊的类,它的格式是:
1 2 3 public enum 枚举类名{ 枚举项1 ,枚举项2 ,枚举项3 ; }
其实枚举项就表示枚举类的对象,只是这些对象在定义枚举类时就预先写好了,以后就只能用这几个固定的对象。
我们用代码演示一下,定义一个枚举类A,在枚举类中定义三个枚举项X, Y, Z
1 2 3 public enum A { X,Y,Z; }
想要获取枚举类中的枚举项,只需要用类名调用就可以了
1 2 3 4 5 6 7 8 public class Test { public static void main (String[] args) { A a1 = A.X; A a2 = A.Y; A a3 = A.Z; } }
刚才说,枚举项实际上是枚举类的对象,这一点其实可以通过反编译的形式来验证(需要用到反编译的命令,这里不能直接将字节码拖进idea反编译)
我们会看到,枚举类A是用class定义的,说明枚举确实是一个类,而且X,Y,Z都是A类的对象;而且每一个枚举项都是被public static final
修饰,所以被可以类名调用,而且不能更改。
2.1.2 枚举深入
既然枚举是一个类的话,我们能不能在枚举类中定义构造器、成员变量、成员方法呢?答案是可以的。来看一下代码吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public enum A { X,Y,Z("张三" ); public A () { } private String name; public A (String name) { this .name=name; } public String getName () { return name; } ... }
虽然枚举类中可以像类一样,写一些类的其他成员,但是一般不会这么写,如果你真要这么干的话,到不如直接写普通类来的直接。
2.2 枚举的应用场景
枚举的应用场景是这样的:枚举一般表示一组信息,然后作为参数进行传输。
我们来看一个案例。比如我们现在有这么一个应用,用户进入应用时,需要让用户选择是女生、还是男生,然后系统会根据用户选择的是男生,还是女生推荐不同的信息给用户观看。
这里我们就可以先定义一个枚举类,用来表示男生、或者女生
1 2 3 public class Constant { BOY,GRIL }
再定义一个测试类,完成用户进入系统后的选择
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class Test { public static void main (String[] args) { provideInfo(Constant.BOY); } public static void provideInfo (Constant c) { switch (c){ case BOY: System.out.println("展示一些信息给男生看" ); break ; case GRIL: System.out.println("展示一些信息给女生看" ); break ; } } }
最终再总结一下枚举的应用场景:枚举一般表示几个固定的值,然后作为参数进行传输 。
三、泛型
3.1 认识泛型
所谓泛型指的是,在定义类、接口、方法时,同时声明了一个或者多个类型变量(如:),称为泛型类、泛型接口、泛型方法、它们统称为泛型。
ArrayList类就是一个泛型类,打开API文档看一下ArrayList类的声明。
ArrayList集合的设计者在定义ArrayList集合时,就已经明确ArrayList集合时给别人装数据用的,但是别人用ArrayList集合时候,装什么类型的数据他不知道,所以就用一个<E>
表示元素的数据类型。
当别人使用ArrayList集合创建对象时,new ArrayList<String>
就表示元素为String类型,new ArrayList<Integer>
表示元素为Integer类型。
我们总结一下泛型的作用、本质:
3.2 自定义泛型类
泛型类,在实际工作中一般都是源代码中写好,我们直接用的,就是ArrayList这样的,自己定义泛型类是非常少的
。
自定义泛型类的格式如下
1 2 3 4 public class 类名<T,W>{ }
接下来,我们自己定义一个MyArrayList泛型类,模拟一下自定义泛型类的使用。注意这里重点仅仅只是模拟泛型类的使用,所以方法中的一些逻辑是次要的,也不会写得太严谨。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class MyArrayList <E>{ private Object[] array = new Object [10 ]; private int index; public void add (E e) { array[index]=e; index++; } public E get (int index) { return (E)array[index]; } }
接下来,我们写一个测试类,来测试自定义的泛型类MyArrayList是否能够正常使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class Test { public static void main (String[] args) { MyArrayList<String> list = new MyArrayList <>(); list.add("张三" ); list.add("李四" ); MyArrayList<Integer> list1 = new MyArrayList <>(); list.add(100 ); list.add(200 ); } }
3.3 自定义泛型接口
泛型接口其实指的是在接口中把不确定的数据类型用<类型变量>
表示。定义格式如下:
1 2 3 4 public interface 接口名<类型变量>{ }
比如,我们现在要做一个系统要处理学生和老师的数据,需要提供2个功能,保存对象数据、根据名称查询数据,要求:这两个功能处理的数据既能是老师对象,也能是学生对象。
首先我们得有一个学生类和老师类
1 2 3 public class Teacher {}
1 2 3 public class Student { }
我们定义一个Data<T>
泛型接口,T表示接口中要处理数据的类型。
1 2 3 4 5 public interface Data <T>{ public void add (T t) ; public ArrayList<T> getByName (String name) ; }
接下来,我们写一个处理Teacher对象的接口实现类
1 2 3 4 5 6 7 8 9 10 11 public class TeacherData implements Data <Teacher>{ public void add (Teacher t) { } public ArrayList<Teacher> getByName (String name) { } }
接下来,我们写一个处理Student对象的接口实现类
1 2 3 4 5 6 7 8 9 10 11 public class StudentData implements Data <Student>{ public void add (Student t) { } public ArrayList<Student> getByName (String name) { } }
3.4 泛型方法
1 2 3 public <泛型变量,泛型变量> 返回值类型 方法名(形参列表){ }
下图中在返回值类型和修饰符之间有定义的才是泛型方法。
接下我们看一个泛型方法的案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Test { public static void main (String[] args) { String rs = test("test" ); Dog d = test(new Dog ()); } public static <T> test(T t){ return t; } }
3.5 泛型限定
学习一个泛型的特殊用法,叫做泛型限定。泛型限定的意思是对泛型的数据类型进行范围的限制。有如下的三种格式
> 表示任意类型
extends 数据类型> 表示指定类型或者指定类型的子类
super 数据类型> 表示指定类型或者指定类型的父类
下面我们演示一下,假设有Car作为父类,BENZ,BWM两个类作为Car的子类,代码如下
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 class Car {}class BENZ extends Car {}class BWN extends Car {}public class Test { public static void main (String[] args) { ArrayList<BWM> list1 = new ArrayList <>(); ArrayList<Benz> list2 = new ArrayList <>(); ArrayList<String> list3 = new ArrayList <>(); test1(list1); test1(list2); test1(list3); ArrayList<Car> list4 = new ArrayList <>(); ArrayList<BWM> list5 = new ArrayList <>(); test2(list4); test2(list5); ArrayList<Car> list6 = new ArrayList <>(); ArrayList<Object> list7 = new ArrayList <>(); test3(list6); test3(list7); } public static void test1 (ArrayList<?> list) { } public static void test2 (ArrayList<? extends Car> list) { } public static void test3 (ArrayList<? super Car> list) { } }
3.6 泛型擦除
泛型擦除。什么意思呢?也就是说泛型只能编译阶段有效,一旦编译成字节码,字节码中是不包含泛型的 。而且泛型只支持引用数据类型,不支持基本数据类型。
把下面的代码的字节码进行反编译
下面是反编译之后的代码,我们发现ArrayList后面没有泛型
四、常用API
API(Application Programming interface)意思是应用程序编程接口,说人话就是Java帮我们写好的一些程序,如:类、方法等,我们直接拿过来用就可以解决一些问题。
我们要学习那些API呢?
“千里之行始于足下,多记、多查、多些代码、孰能生巧!”
4.1 Object类
Object类。Object类是Java中所有类的祖宗类,因此,Java中所有类的对象都可以直接使用Object类中提供的一些方法。
按照下图的提示,可以搜索到你想要找的类
我们找到Object类的下面两个方法
我们先来学习toString()方法。
1 2 3 public String toString () 调用toString()方法可以返回对象的字符串表示形式。 默认的格式是:“包名.类名@哈希值16 进制”
假设有一个学生类如下
1 2 3 4 5 6 7 8 9 public class Student { private String name; private int age; public Student (String name, int age) { this .name=name; this .age=age; } }
再定义一个测试类
1 2 3 4 5 6 public class Test { public static void main (String[] args) { Student s1 = new Student ("赵敏" ,23 ); System.out.println(s1.toString()); } }
打印结果如下
如果,在Student类重写toString()方法,那么我们可以返回对象的属性值,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Student { private String name; private int age; public Student (String name, int age) { this .name=name; this .age=age; } @Override public String toString () { return "Student{name=‘" +name+"’, age=" +age+"}" ; } }
运行测试类,结果如下
4.1.2 equals(Object o)方法
接下来,我们学习一下Object类的equals方法
1 2 public boolean equals (Object o) 判断此对象与参数对象是否"相等"
我们写一个测试类,测试一下
1 2 3 4 5 6 7 8 9 10 11 public class Test { public static void main (String[] args) { Student s1 = new Student ("赵薇" ,23 ); Student s2 = new Student ("赵薇" ,23 ); System.out.println(s1.equals(s2)); System.out.println(s1==s2); } }
但是如果我们在Student类中,把equals方法重写了,就按照对象的属性值进行比较
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 Student { private String name; private int age; public Student (String name, int age) { this .name=name; this .age=age; } @Override public String toString () { return "Student{name=‘" +name+"’, age=" +age+"}" ; } @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 ; return name != null ? name.equals(student.name) : student.name == null ; } }
再运行测试类,效果如下
总结一下Object的toString方法和equals方法
1 2 3 4 5 6 7 public String toString () 返回对象的字符串表示形式。默认的格式是:“包名.类名@哈希值16 进制” 【子类重写后,返回对象的属性值】 public boolean equals (Object o) 判断此对象与参数对象是否"相等" 。默认比较对象的地址值,和"==" 没有区别 【子类重写后,比较对象的属性值】
4.1.3 clone() 方法
接下来,我们学习Object类的clone()方法,克隆。意思就是某一个对象调用这个方法,这个方法会复制一个一模一样的新对象,并返回。
1 2 public Object clone () 克隆当前对象,返回一个新对象
想要调用clone()方法,必须让被克隆的类实现Cloneable接口。如我们准备克隆User类的对象,代码如下
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 User implements Cloneable { private String id; private String username; private String password; private double [] scores; public User () { } public User (String id, String username, String password, double [] scores) { this .id = id; this .username = username; this .password = password; this .scores = scores; } @Override protected Object clone () throws CloneNotSupportedException { return super .clone(); } }
接着,我们写一个测试类,克隆User类的对象。并观察打印的结果
1 2 3 4 5 6 7 8 9 10 11 public class Test { public static void main (String[] args) throws CloneNotSupportedException { User u1 = new User (1 ,"zhangsan" ,"wo666" ,new double []{99.0 ,99.5 }); User u2 = (User) u1.clone(); System.out.println(u2.getId()); System.out.println(u2.getUsername()); System.out.println(u2.getPassword()); System.out.println(u2.getScores()); } }
我们发现,克隆得到的对象u2它的属性值和原来u1对象的属性值是一样的。
上面演示的克隆方式,是一种浅克隆的方法,浅克隆的意思:拷贝出来的对象封装的数据与原对象封装的数据一模一样(引用类型拷贝的是地址值) 。如下图所示
还有一种拷贝方式,称之为深拷贝,拷贝原理如下图所示
下面演示一下深拷贝User对象
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 User implements Cloneable { private String id; private String username; private String password; private double [] scores; public User () { } public User (String id, String username, String password, double [] scores) { this .id = id; this .username = username; this .password = password; this .scores = scores; } @Override protected Object clone () throws CloneNotSupportedException { User u = (User) super .clone(); u.scores = u.scores.clone(); return u; } }
4.2 Objects类
Objects是一个工具类,提供了一些方法可以对任意对象进行操作。主要方法如下
下面写代码演示一下这几个方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class Test { public static void main (String[] args) { String s1 = null ; String s2 = "itheima" ; System.out.println(s1.equals(s2)); System.out.println(Objects.equals(s1,s2)); System.out.println(Objects.isNull(s1)); System.out.println(s1==null ); System.out.println(Objects.nonNull(s2)); System.out.println(s2!=null ); } }
4.3 基本类型包装类
为什么要学习包装类呢?因为在Java中有一句很经典的话,万物皆对象。Java中的8种基本数据类型还不是对象,所以要把它们变成对象,变成对象之后,可以提供一些方法对数据进行操作。
Java中8种基本数据类型都用一个包装类与之对一个,如下图所示
学习包装类两要点:
创建包装类的对象方式、自动装箱和拆箱的特性;
利用包装类提供的方法对字符串和基本类型数据进行相互转换
4.2.1 创建包装类对象
我们先来学习,创建包装类对象的方法,以及包装类的一个特性叫自动装箱和自动拆箱。我们以Integer为例,其他的都是类似的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Integer a = new Integer (10 );Integer b = Integer.valueOf(10 );Integer c = 10 ;int d = c;ArrayList<Integer> list = new ArrayList <>(); list.add(100 ); int e = list.get(0 );
4.2.2 包装类数据类型转换
在开发中,经常使用包装类对字符串和基本类型数据进行相互转换。
把字符串转换为数值型数据:包装类.parseXxx(字符串)
1 2 public static int parseInt (String s) 把字符串转换为基本数据类型
将数值型数据转换为字符串:包装类.valueOf(数据);
1 2 public static String valueOf (int a) 把基本类型数据转换为
1 2 3 4 5 6 7 8 9 10 11 12 13 String ageStr = "29" ;int age1 = Integer.parseInt(ageStr);String scoreStr = 3.14 ;double score = Double.prarseDouble(scoreStr);Integer a = 23 ;String s1 = Integer.toString(a);String s2 = a.toString();String s3 = a+"" ;String s4 = String.valueOf(a);
常用API
一、 StringBuilder类
StringBuilder代表可变字符串对象,相当于是一个容器,它里面的字符串是可以改变的,就是用来操作字符串的。
好处:StringBuilder比String更合适做字符串的修改操作,效率更高,代码也更加简洁。
1.1 StringBuilder方法演示
接下来我们用代码演示一下StringBuilder的用法
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 Test { public static void main (String[] args) { StringBuilder sb = new StringBuilder ("itehima" ); sb.append(12 ); sb.append("黑马" ); sb.append(true ); sb.append(666 ).append("黑马2" ).append(666 ); System.out.println(sb); sb.reverse(); System.out.println(sb); System.out.println(sb.length()); String s = sb.toString(); System.out.println(s); } }
为什么要用StringBuilder对字符串进行操作呢?因为它的效率比String更高,我们可以下面两段代码验证一下。
经过验证,直接使用Stirng拼接100万次,等了1分钟,还没结束,我等不下去了;但是使用StringBuilder做拼接,不到1秒钟出结果了。
1.2 StringBuilder应用案例
通过一个案例把StringBuilder运用下,案例需求如下图所示
代码如下
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 Test { public static void main (String[] args) { String str = getArrayData( new int []{11 ,22 ,33 }); System.out.println(str); } public static String getArrayData (int [] arr) { if (arr==null ){ return null ; } StringBuilder sb = new StringBuilder ("[" ); for (int i=0 ; i<arr.length; i++){ if (i==arr.legnth-1 ){ sb.append(arr[i]).append("]" );; }else { sb.append(arr[i]).append("," ); } } return sb.toString(); } }
二、StringJoiner类
StringJoiner号称是拼接神器,不仅效率高,而且代码简洁。
下面演示一下StringJoiner的基本使用
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) { StringJoiner s = new StringJoiner ("," ); s.add("java1" ); s.add("java2" ); s.add("java3" ); System.out.println(s); StringJoiner s1 = new StringJoiner ("," ,"[" ,"]" ); s1.add("java1" ); s1.add("java2" ); s1.add("java3" ); System.out.println(s1); } }
使用StirngJoiner改写前面把数组转换为字符串的案例
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) { String str = getArrayData( new int []{11 ,22 ,33 }); System.out.println(str); } public static String getArrayData (int [] arr) { if (arr==null ){ return null ; } StringJoiner s = new StringJoiner (", " ,"[" ,"]" ); for (int i=0 ; i<arr.length; i++){ s.add(String.valueOf(arr[i])); } return s.toString(); } }
三、Math类
Math是数学的意思,该类提供了很多个进行数学运算的方法,如求绝对值,求最大值,四舍五入等,话不多说,直接上代码。
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 MathTest { public static void main (String[] args) { System.out.println(Math.abs(-12 )); System.out.println(Math.abs(123 )); System.out.println(Math.abs(-3.14 )); System.out.println(Math.ceil(4.0000001 )); System.out.println(Math.ceil(4.0 )); System.out.println(Math.floor(4.999999 )); System.out.println(Math.floor(4.0 )); System.out.println(Math.round(3.4999 )); System.out.println(Math.round(3.50001 )); System.out.println(Math.max(10 , 20 )); System.out.println(Math.min(10 , 20 )); System.out.println(Math.pow(2 , 3 )); System.out.println(Math.pow(3 , 2 )); System.out.println(Math.random()); } }
四、 System类
System类,这是系统类,提供了一些获取获取系统数据的方法。比如获取系统时间。
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 SystemTest { public static void main (String[] args) { System.exit(0 ); long time = System.currentTimeMillis(); System.out.println(time); for (int i = 0 ; i < 1000000 ; i++) { System.out.println("输出了:" + i); } long time2 = System.currentTimeMillis(); System.out.println((time2 - time) / 1000.0 + "s" ); } }
五、Runtime类
Java的运行时类,叫Runtime类。这个类可以用来获取JVM的一些信息,也可以用这个类去执行其他的程序。
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 RuntimeTest { public static void main (String[] args) throws IOException, InterruptedException { Runtime r = Runtime.getRuntime(); System.out.println(r.availableProcessors()); System.out.println(r.totalMemory()/1024.0 /1024.0 + "MB" ); System.out.println(r.freeMemory()/1024.0 /1024.0 + "MB" ); Process p = r.exec("QQ" ); Thread.sleep(5000 ); p.destroy(); } }
六、BigDecimal类
我们先看一段代码,看这个代码有什么问题?再说BigDeimal这个类是干什么用的,这样会更好理解一些。
1 2 3 4 5 6 7 8 public class Test { public static void main (String[] args) { System.out.println(0.1 + 0.2 ); System.out.println(1.0 - 0.32 ); System.out.println(1.015 * 100 ); System.out.println(1.301 / 100 ); } }
运行以上代码,我们会发现,结果并和我们想看到的不太一样。如下图所示
为了解决计算精度损失的问题,Java给我们提供了BigDecimal类,它提供了一些方法可以对数据进行四则运算,而且不丢失精度,同时还可以保留指定的小数位。下面看代码,演示一下
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 public class Test2 { public static void main (String[] args) { double a = 0.1 ; double b = 0.2 ; BigDecimal a1 = BigDecimal.valueOf(a); BigDecimal b1 = BigDecimal.valueOf(b); BigDecimal c1 = a1.add(b1); System.out.println(c1); BigDecimal c2 = a1.subtract(b1); System.out.println(c2); BigDecimal c3 = a1.multiply(b1); System.out.println(c3); BigDecimal c4 = a1.divide(b1); System.out.println(c4); BigDecimal d1 = BigDecimal.valueOf(0.1 ); BigDecimal d2 = BigDecimal.valueOf(0.3 ); BigDecimal d3 = d1.divide(d2, 2 , RoundingMode.HALF_UP); System.out.println(d3); double db1 = d3.doubleValue(); double db2 = c1.doubleValue(); print(db1); print(db2); } public static void print (double a) { System.out.println(a); } }
五、Date类
Date类,Java中是由这个类的对象用来表示日期或者时间。
Date对象记录的时间是用毫秒值来表示的。Java语言规定,1970年1月1日0时0分0秒认为是时间的起点,此时记作0,那么1000(1秒=1000毫秒)就表示1970年1月1日0时0分1秒,依次类推。
下面是Date类的构造方法,和常见的成员方法,利用这些API写代码尝试一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class Test1Date { public static void main (String[] args) { Date d = new Date (); System.out.println(d); long time = d.getTime(); System.out.println(time); time += 2 * 1000 ; Date d2 = new Date (time); System.out.println(d2); Date d3 = new Date (); d3.setTime(time); System.out.println(d3); } }
SimpleDateFormat类就可以转换Date对象表示日期时间的显示格式。
接下来,我们先演示一下日期格式化,需要用到如下的几个方法
注意:创建SimpleDateFormat对象时,在构造方法的参数位置传递日期格式,而日期格式是由一些特定的字母拼接而来的。我们需要记住常用的几种日期/时间格式
1 2 3 4 5 6 7 8 9 10 11 12 字母 表示含义 yyyy 年 MM 月 dd 日 HH 时 mm 分 ss 秒 SSS 毫秒 "2022年12月12日" 的格式是 "yyyy年MM月dd日" "2022-12-12 12:12:12" 的格式是 "yyyy-MM-dd HH:mm:ss" 按照上面的格式可以任意拼接,但是字母不能写错
最后,上代码演示一下
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 Test2SimpleDateFormat { public static void main (String[] args) throws ParseException { Date d = new Date (); System.out.println(d); long time = d.getTime(); System.out.println(time); SimpleDateFormat sdf = new SimpleDateFormat ("yyyy年MM月dd日 HH:mm:ss EEE a" ); String rs = sdf.format(d); String rs2 = sdf.format(time); System.out.println(rs); System.out.println(rs2); System.out.println("----------------------------------------------" ); String dateStr = "2022-12-12 12:12:11" ; SimpleDateFormat sdf2 = new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss" ); Date d2 = sdf2.parse(dateStr); System.out.println(d2); } }
七、Calendar类
Calendar类表示日历,它提供了一些比Date类更好用的方法。
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 public class Test4Calendar { public static void main (String[] args) { Calendar now = Calendar.getInstance(); System.out.println(now); int year = now.get(Calendar.YEAR); System.out.println(year); int days = now.get(Calendar.DAY_OF_YEAR); System.out.println(days); Date d = now.getTime(); System.out.println(d); long time = now.getTimeInMillis(); System.out.println(time); now.set(Calendar.MONTH, 9 ); now.set(Calendar.DAY_OF_YEAR, 125 ); System.out.println(now); now.add(Calendar.DAY_OF_YEAR, 100 ); now.add(Calendar.DAY_OF_YEAR, -10 ); now.add(Calendar.DAY_OF_MONTH, 6 ); now.add(Calendar.HOUR, 12 ); now.set(2026 , 11 , 22 ); System.out.println(now); } }
八、为什么JDK8要新增日期类
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 Test { public static void main (String[] args) { Date d = new Date (); Calendar c = Calendar.getInstance(); int year = c.get(Calendar.YEAR); System.out.println(year); } }
九、JDK8日期、时间、日期时间
JDK8新增的日期类。为什么以前的Date类就可以表示日期,为什么要有新增的日期类呢?原因如下
JDK8新增的日期类分得更细致一些,比如表示年月日用LocalDate类、表示时间秒用LocalTime类、而表示年月日时分秒用LocalDateTime类等;除了这些类还提供了对时区、时间间隔进行操作的类等。它们几乎把对日期/时间的所有操作都通过了API方法,用起来特别方便。
先学习表示日期、时间、日期时间的类;有LocalDate、LocalTime、以及LocalDateTime类。仔细阅读代码,你会发现这三个类的用法套路都是一样的。
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 Test1_LocalDate { public static void main (String[] args) { LocalDate ld = LocalDate.now(); System.out.println(ld); int year = ld.getYear(); int month = ld.getMonthValue(); int day = ld.getDayOfMonth(); int dayOfYear = ld.getDayOfYear(); int dayOfWeek = ld.getDayOfWeek().getValue(); System.out.println(year); System.out.println(day); System.out.println(dayOfWeek); LocalDate ld2 = ld.withYear(2099 ); LocalDate ld3 = ld.withMonth(12 ); System.out.println(ld2); System.out.println(ld3); System.out.println(ld); LocalDate ld4 = ld.plusYears(2 ); LocalDate ld5 = ld.plusMonths(2 ); LocalDate ld6 = ld.minusYears(2 ); LocalDate ld7 = ld.minusMonths(2 ); LocalDate ld8 = LocalDate.of(2099 , 12 , 12 ); LocalDate ld9 = LocalDate.of(2099 , 12 , 12 ); System.out.println(ld8.equals(ld9)); System.out.println(ld8.isAfter(ld)); System.out.println(ld8.isBefore(ld)); } }
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 Test2_LocalTime { public static void main (String[] args) { LocalTime lt = LocalTime.now(); System.out.println(lt); int hour = lt.getHour(); int minute = lt.getMinute(); int second = lt.getSecond(); int nano = lt.getNano(); LocalTime lt3 = lt.withHour(10 ); LocalTime lt4 = lt.withMinute(10 ); LocalTime lt5 = lt.withSecond(10 ); LocalTime lt6 = lt.withNano(10 ); LocalTime lt7 = lt.plusHours(10 ); LocalTime lt8 = lt.plusMinutes(10 ); LocalTime lt9 = lt.plusSeconds(10 ); LocalTime lt10 = lt.plusNanos(10 ); LocalTime lt11 = lt.minusHours(10 ); LocalTime lt12 = lt.minusMinutes(10 ); LocalTime lt13 = lt.minusSeconds(10 ); LocalTime lt14 = lt.minusNanos(10 ); LocalTime lt15 = LocalTime.of(12 , 12 , 12 ); LocalTime lt16 = LocalTime.of(12 , 12 , 12 ); System.out.println(lt15.equals(lt16)); System.out.println(lt15.isAfter(lt)); System.out.println(lt15.isBefore(lt)); } }
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 public class Test3_LocalDateTime { public static void main (String[] args) { LocalDateTime ldt = LocalDateTime.now(); System.out.println(ldt); int year = ldt.getYear(); int month = ldt.getMonthValue(); int day = ldt.getDayOfMonth(); int dayOfYear = ldt.getDayOfYear(); int dayOfWeek = ldt.getDayOfWeek().getValue(); int hour = ldt.getHour(); int minute = ldt.getMinute(); int second = ldt.getSecond(); int nano = ldt.getNano(); LocalDateTime ldt2 = ldt.withYear(2029 ); LocalDateTime ldt3 = ldt.withMinute(59 ); LocalDateTime ldt4 = ldt.plusYears(2 ); LocalDateTime ldt5 = ldt.plusMinutes(3 ); LocalDateTime ldt6 = ldt.minusYears(2 ); LocalDateTime ldt7 = ldt.minusMinutes(3 ); LocalDateTime ldt8 = LocalDateTime.of(2029 , 12 , 12 , 12 , 12 , 12 , 1222 ); LocalDateTime ldt9 = LocalDateTime.of(2029 , 12 , 12 , 12 , 12 , 12 , 1222 ); System.out.println(ldt9.equals(ldt8)); System.out.println(ldt9.isAfter(ldt)); System.out.println(ldt9.isBefore(ldt)); LocalDate ld = ldt.toLocalDate(); LocalTime lt = ldt.toLocalTime(); LocalDateTime ldt10 = LocalDateTime.of(ld, lt); } }
十、JDK8日期(时区)
代表时区的两个类。由于世界各个国家与地区的经度不同,各地区的时间也有所不同,因此会划分为不同的时区。每一个时区的时间也不太一样。
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 Test4_ZoneId_ZonedDateTime { public static void main (String[] args) { ZoneId zoneId = ZoneId.systemDefault(); System.out.println(zoneId.getId()); System.out.println(zoneId); System.out.println(ZoneId.getAvailableZoneIds()); ZoneId zoneId1 = ZoneId.of("America/New_York" ); ZonedDateTime now = ZonedDateTime.now(zoneId1); System.out.println(now); ZonedDateTime now1 = ZonedDateTime.now(Clock.systemUTC()); System.out.println(now1); ZonedDateTime now2 = ZonedDateTime.now(); System.out.println(now2); } }
十一、JDK8日期(Instant类)
Instant类。通过获取Instant的对象可以拿到此刻的时间,该时间由两部分组成:从1970-01-01 00:00:00 开始走到此刻的总秒数+不够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 Test5_Instant { public static void main (String[] args) { Instant now = Instant.now(); long second = now.getEpochSecond(); System.out.println(second); int nano = now.getNano(); System.out.println(nano); System.out.println(now); Instant instant = now.plusNanos(111 ); Instant now1 = Instant.now(); Instant now2 = Instant.now(); LocalDateTime l = LocalDateTime.now(); } }
十二、JDK8日期(格式化器)
日期格式化类,叫DateTimeFormater。它可以从来对日期进行格式化和解析。它代替了原来的SimpleDateFormat类。
需要用到的方法,如下图所示
接下来,将上面的方法用代码来演示一下
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 Test6_DateTimeFormatter { public static void main (String[] args) { DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss" ); LocalDateTime now = LocalDateTime.now(); System.out.println(now); String rs = formatter.format(now); System.out.println(rs); String rs2 = now.format(formatter); System.out.println(rs2); String dateStr = "2029年12月12日 12:12:11" ; LocalDateTime ldt = LocalDateTime.parse(dateStr, formatter); System.out.println(ldt); } }
十三、JDK8日期(Period类)
除以了上新增的类,JDK8还补充了两个类,一个叫Period类、一个叫Duration类;这两个类可以用来对计算两个时间点的时间间隔。
其中Period用来计算日期间隔(年、月、日),Duration用来计算时间间隔(时、分、秒、纳秒)
先来演示Period类的用法,它的方法如下图所示。可以用来计算两个日期之间相隔的年、相隔的月、相隔的日。只能两个计算LocalDate对象之间的间隔
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class Test7_Period { public static void main (String[] args) { LocalDate start = LocalDate.of(2029 , 8 , 10 ); LocalDate end = LocalDate.of(2029 , 12 , 15 ); Period period = Period.between(start, end); System.out.println(period.getYears()); System.out.println(period.getMonths()); System.out.println(period.getDays()); } }
十四、JDK8日期(Duration类)
Duration类。它是用来表示两个时间对象的时间间隔。可以用于计算两个时间对象相差的天数、小时数、分数、秒数、纳秒数;支持LocalTime、LocalDateTime、Instant等时间
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class Test8_Duration { public static void main (String[] args) { LocalDateTime start = LocalDateTime.of(2025 , 11 , 11 , 11 , 10 , 10 ); LocalDateTime end = LocalDateTime.of(2025 , 11 , 11 , 11 , 11 , 11 ); Duration duration = Duration.between(start, end); System.out.println(duration.toDays()); System.out.println(duration.toHours()); System.out.println(duration.toMinutes()); System.out.println(duration.toSeconds()); System.out.println(duration.toMillis()); System.out.println(duration.toNanos()); } }
Lambda、方法引用、正则表达式
一、Lambda表达式
学习一个JDK8新增的一种语法形式,叫做Lambda表达式。作用:用于简化匿名内部类代码的书写。
1.1 Lambda表达式基本使用
怎么去简化呢?Lamdba是有特有的格式的,按照下面的格式来编写Lamdba。
1 2 3 (被重写方法的形参列表) -> { 被重写方法的方法体代码; }
需要给说明一下的是,在使用Lambda表达式之前,必须先有一个接口,而且接口中只能有一个抽象方法。(注意:不能是抽象类,只能是接口)
像这样的接口,我们称之为函数式接口,只有基于函数式接口的匿名内部类才能被Lambda表达式简化。
1 2 3 public interface Swimming { void swim () ; }
有了以上的Swimming接口之后,接下来才能再演示,使用Lambda表达式,简化匿名内部类书写。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class LambdaTest1 { public static void main (String[] args) { Swimming s = new Swimming (){ @Override public void swim () { System.out.println("学生快乐的游泳~~~~" ); } }; s.swim(); Swimming s1 = () -> { System.out.println("学生快乐的游泳~~~~" ); }; s1.swim(); } }
好的,我们现在已经知道Lamdba表达式可以简化基于函数式接口的匿名内部类的书写。接下来,我们可以使用Arrays方法时的代码,使用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 27 28 29 30 31 32 33 34 35 36 37 38 39 public class LambdaTest2 { public static void main (String[] args) { double [] prices = {99.8 , 128 , 100 }; Arrays.setAll(prices, new IntToDoubleFunction () { @Override public double applyAsDouble (int value) { return prices[value] * 0.8 ; } }); Arrays.setAll(prices, (int value) -> { return prices[value] * 0.8 ; }); System.out.println(Arrays.toString(prices)); System.out.println("-----------------------------------------------" ); Student[] students = new Student [4 ]; students[0 ] = new Student ("蜘蛛精" , 169.5 , 23 ); students[1 ] = new Student ("紫霞" , 162.8 , 26 ); students[2 ] = new Student ("紫霞" , 162.8 , 26 ); students[3 ] = new Student ("至尊宝" , 167.5 , 24 ); Arrays.sort(students, new Comparator <Student>() { @Override public int compare (Student o1, Student o2) { return Double.compare(o1.getHeight(), o2.getHeight()); } }); Arrays.sort(students, (Student o1, Student o2) -> { return Double.compare(o1.getHeight(), o2.getHeight()); }); System.out.println(Arrays.toString(students)); } }
1.2 Lambda表达式省略规则
Lamdba表达式的几种简化写法。具体的简化规则如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 1. Lambda的标准格式 (参数类型1 参数名1 , 参数类型2 参数名2 )->{ ...方法体的代码... return 返回值; } 2. 在标准格式的基础上()中的参数类型可以直接省略 (参数名1 , 参数名2 )->{ ...方法体的代码... return 返回值; } 2. 如果{}总的语句只有一条语句,则{}可以省略、return 关键字、以及最后的“;”都可以省略 (参数名1 , 参数名2 )-> 结果 4. 如果()里面只有一个参数,则()可以省略 (参数名)->结果
接下来从匿名内部类开始、到Lambda标准格式、再到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 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 public class LambdaTest2 { public static void main (String[] args) { double [] prices = {99.8 , 128 , 100 }; Arrays.setAll(prices, new IntToDoubleFunction () { @Override public double applyAsDouble (int value) { return prices[value] * 0.8 ; } }); Arrays.setAll(prices, (int value) -> { return prices[value] * 0.8 ; }); Arrays.setAll(prices, (value) -> { return prices[value] * 0.8 ; }); Arrays.setAll(prices, value -> { return prices[value] * 0.8 ; }); Arrays.setAll(prices, value -> prices[value] * 0.8 ); System.out.println(Arrays.toString(prices)); System.out.println("------------------------------------ Student[] students = new Student[4]; students[0] = new Student(" 蜘蛛精", 169.5, 23); students[1] = new Student(" 紫霞", 162.8, 26); students[2] = new Student(" 紫霞", 162.8, 26); students[3] = new Student(" 至尊宝", 167.5, 24); //1.使用匿名内部类 Arrays.sort(students, new Comparator<Student>() { @Override public int compare(Student o1, Student o2) { return Double.compare(o1.getHeight(), o2.getHeight()); // 升序 } }); //2.使用Lambda表达式表达式——标准格式 Arrays.sort(students, (Student o1, Student o2) -> { return Double.compare(o1.getHeight(), o2.getHeight()); // 升序 }); //2.使用Lambda表达式表达式——省略参数类型 Arrays.sort(students, ( o1, o2) -> { return Double.compare(o1.getHeight(), o2.getHeight()); // 升序 }); //4.使用Lambda表达式表达式——省略{} Arrays.sort(students, ( o1, o2) -> Double.compare(o1.getHeight(), o2.getHeight())); System.out.println(Arrays.toString(students)); } }
二、JDK8新特性(方法引用)
JDK8的另一个新特性,叫做方法引用。我们知道Lambda是用来简化匿名代码的书写格式的,而方法引用是用来进一步简化Lambda表达式的,它简化的更加过分。
2.1 静态方法引用
我们先学习静态方法的引用,还是用之前Arrays代码来做演示。现在准备好下面的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class Test1 { public static void main (String[] args) { Student[] students = new Student [4 ]; students[0 ] = new Student ("蜘蛛精" , 169.5 , 23 ); students[1 ] = new Student ("紫霞" , 162.8 , 26 ); students[2 ] = new Student ("紫霞" , 162.8 , 26 ); students[3 ] = new Student ("至尊宝" , 167.5 , 24 ); Arrays.sort(students, new Comparator <Student>() { @Override public int compare (Student o1, Student o2) { return o1.getAge() - o2.getAge(); } }); Arrays.sort(students, (o1, o2) -> o1.getAge() - o2.getAge()); } }
现在,我想要把下图中Lambda表达式的方法体,用一个静态方法代替
准备另外一个类CompareByData类,用于封装Lambda表达式的方法体代码;
1 2 3 4 5 public class CompareByData { public static int compareByAge (Student o1, Student o2) { return o1.getAge() - o2.getAge(); } }
现在我们就可以把Lambda表达式的方法体代码,改为下面的样子
1 Arrays.sort(students, (o1, o2) -> CompareByData.compareByAge(o1, o2));
Java为了简化上面Lambda表达式的写法,利用方法引用可以改进为下面的样子。**实际上就是用类名调用方法,但是把参数给省略了。**这就是静态方法引用
1 2 Arrays.sort(students, CompareByData::compareByAge);
2.2 实例方法引用
实例方法的引用。现在,我想要把下图中Lambda表达式的方法体,用一个实例方法代替。
在CompareByData类中,再添加一个实例方法,用于封装Lambda表达式的方法体
接下来,我们把Lambda表达式的方法体,改用对象调用方法
1 2 CompareByData compare = new CompareByData ();Arrays.sort(students, (o1, o2) -> compare.compareByAgeDesc(o1, o2));
最后,再将Lambda表达式的方法体,直接改成方法引用写法。实际上就是用类名调用方法,但是省略的参数 。这就是实例方法引用
1 2 CompareByData compare = new CompareByData ();Arrays.sort(students, compare::compareByAgeDesc);
给小伙伴的寄语:一定要按照老师写的步骤,一步一步来做,你一定能学会的!!!
2.3 特定类型的方法引用
1 2 3 4 Java约定: 如果某个Lambda表达式里只是调用一个实例方法,并且前面参数列表中的第一个参数作为方法的主调, 后面的所有参数都是作为该实例方法的入参时,则就可以使用特定类型的方法引用。 格式: 类型::方法名
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class Test2 { public static void main (String[] args) { String[] names = {"boby" , "angela" , "Andy" ,"dlei" , "caocao" , "Babo" , "jack" , "Cici" }; Arrays.sort(names, new Comparator <String>() { @Override public int compare (String o1, String o2) { return o1.compareToIgnoreCase(o2); } }); Arrays.sort(names, ( o1, o2) -> o1.compareToIgnoreCase(o2) ); Arrays.sort(names, String::compareToIgnoreCase); System.out.println(Arrays.toString(names)); } }
2.4 构造器引用
现在,我们准备一个JavaBean类,Car类
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 Car { private String name; private double price; public Car () { } public Car (String name, double price) { this .name = name; this .price = price; } public String getName () { return name; } public void setName (String name) { this .name = name; } public double getPrice () { return price; } public void setPrice (double price) { this .price = price; } @Override public String toString () { return "Car{" + "name='" + name + '\'' + ", price=" + price + '}' ; } }
因为方法引用是基于Lamdba表达式简化的,所以也要按照Lamdba表达式的使用前提来用,需要一个函数式接口,接口中代码的返回值类型是Car类型
1 2 3 interface CreateCar { Car create (String name, double price) ; }
最后,再准备一个测试类,在测试类中创建CreateCar接口的实现类对象,先用匿名内部类创建、再用Lambda表达式创建,最后改用方法引用创建。同学们只关注格式就可以,不要去想为什么(语法就是这么设计的)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class Test3 { public static void main (String[] args) { CreateCar cc1 = new CreateCar (){ @Override public Car create (String name, double price) { return new Car (name, price); } }; CreateCar cc2 = (name, price) -> new Car (name, price); CreateCar cc3 = Car::new ; Car car = cc2.create("奔驰" , 49.9 ); System.out.println(car); } }
三、正则表达式
正则表达式其实是由一些特殊的符号组成的,它代表的是某种规则。
正则表达式的作用1:用来校验字符串数据是否合法
正则表达式的作用2:可以从一段文本中查找满足要求的内容
3.1 正则表达式初体验
以QQ号码为例,来体验一下正则表达式的用法。注意:现在仅仅只是体验而已,我们还没有讲正则表达式的具体写法。
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}" ); }
我们发现,使用正则表达式,大大简化的了代码的写法。这个代码现在不用写,体验到正则表达式的优势就可以了。
3.2 正则表达式书写规则
前面我们已经体验到了正则表达式,可以简化校验数据的代码书写。这里需要用到一个方法叫matches(String regex)
。这个方法时属于String类的方法。
这个方法是用来匹配一个字符串是否匹配正则表达式的规则,参数需要调用者传递一个正则表达式。但是正则表达式不能乱写,是有特定的规则的。
在API中有一个类叫做Pattern,我们可以到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 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)+" )); } }
3.3 正则表达式应用案例
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("您输入的邮箱格式不正确~~~" ); } } } }
3.4 正则表达式信息爬取
正则表达式的第二个作用:在一段文本中查找满足要求的内容
我们还是通过一个案例给大家做演示:案例需求如下
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 RegexTest4 { public static void main (String[] args) { method1(); } public static void method1 () { String data = " 来黑马程序员学习Java,\n" + " 电话:1866668888,18699997777\n" + " 或者联系邮箱:boniu@itcast.cn,\n" + " 座机电话:01036517895,010-98951256\n" + " 邮箱:bozai@itcast.cn,\n" + " 邮箱:dlei0009@162.com,\n" + " 热线电话:400-618-9090 ,400-618-4000,4006184000,4006189090" ; String regex = "(1[3-9]\\d{9})|(0\\d{2,7}-?[1-9]\\d{4,19})|(\\w{2,}@\\w{2,20}(\\.\\w{2,10}){1,2})" + "|(400-?\\d{3,7}-?\\d{3,7})" ; Pattern pattern = Pattern.compile(regex); Matcher matcher = pattern.matcher(data); while (matcher.find()){ String rs = matcher.group(); System.out.println(rs); } } }
3.5 正则表达式搜索、替换
正则表达式的另外两个功能,替换、分割的功能。需要注意的是这几个功能需要用到Stirng类中的方法。这两个方法其实我们之前学过,只是当时没有学正则表达式而已。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class RegexTest5 { public static void main (String[] args) { String s1 = "古力娜扎ai8888迪丽热巴999aa5566马尔扎哈fbbfsfs42425卡尔扎巴" ; System.out.println(s1.replaceAll("\\w+" , "-" )); String s2 = "我我我喜欢编编编编编编编编编编编编程程程" ; System.out.println(s2.replaceAll("(.)\\1+" , "$1" )); String s3 = "古力娜扎ai8888迪丽热巴999aa5566马尔扎哈fbbfsfs42425卡尔扎巴" ; String[] names = s2.split("\\w+" ); System.out.println(Arrays.toString(names)); } }
异常、集合进阶
一、异常
1.1 认识异常
先来演示一个运行时异常产生
1 2 3 int [] arr = {11 ,22 ,33 };System.out.println(arr[5 ]);
下图是API中对ArrayIndexOutOfBoundsExcpetion类的继承体系,以及告诉我们它在什么情况下产生。
再来演示一个编译时异常
我们在调用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(); } } }
1.2 自定义异常
我们通过一个实际场景,来给大家演示自定义异常。
需求:写一个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 声明。
1.3 异常处理
比如有如下的场景:A调用用B,B调用C;C中有异常产生抛给B,B中有异常产生又抛给A;异常到了A这里就不建议再抛出了,因为最终抛出被JVM处理程序就会异常终止,并且给用户看异常信息,用户也看不懂,体验很不好。
此时比较好的做法就是:1.将异常捕获,将比较友好的信息显示给用户看;2.尝试重新执行,看是是否能修复这个问题。
我们看一个代码,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.webp" ); } }
第二种处理方式是:在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("您输入的价格是不合适的!" ); } } } }
二、集合概述和分类
2.1 集合的分类
对这些集合进行分类学习,如下图所示:一类是单列集合元素是一个一个的,另一类是双列集合元素是一对一对的。
Collection单列集合。Collection是单列集合的根接口,Collection接口下面又有两个子接口List接口、Set接口,List和Set下面分别有不同的实现类,如下图所示:
上图中各种集合的特点如下图所示:
可以自己写代码验证一下,各种集合的特点
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);
2.2 Collection集合的常用方法
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遍历方式
3.1 迭代器遍历集合
代码写法如下:
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()方法获取元素,并将当月元素指向下一个位置,
等下次循环时,则获取下一个元素,依此内推
最后,我们再总结一下,使用迭代器遍历集合用到哪些方法
3.2 增强for遍历集合
格式如下:
1 2 3 for (元素的数据类型 变量名:数组或者集合){ }
需要注意的是,增强for不光可以遍历集合,还可以遍历数组。接下来我们用代码演示一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Collection<String> c = new ArrayList <>(); c.add("赵敏" ); c.add("小昭" ); c.add("素素" ); c.add("灭绝" ); for (String s: c){ System.out.println(s); } String[] arr = {"迪丽热巴" , "古力娜扎" , "稀奇哈哈" }; for (String name: arr){ System.out.println(name); }
3.3 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));
四、List系列集合
4.1 List集合的常用方法
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);
4.2 List集合的遍历方式
List集合相比于前面的Collection多了一种可以通过索引遍历的方式,所以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));
4.3 ArrayList底层的原理
ArrayList集合底层是基于数组结构实现的,也就是说当你往集合容器中存储元素时,底层本质上是往数组中存储元素。 特点如下:
我们知道数组的长度是固定的,但是集合的长度是可变的,这是怎么做到的呢?原理如下:
数组扩容,并不是在原数组上扩容(原数组是不可以扩容的),底层是创建一个新数组,然后把原数组中的元素全部复制到新数组中去。
4.4 LinkedList底层原理
LinkedList底层是链表结构,链表结构是由一个一个的节点组成,一个节点由数据值、下一个元素的地址组成。如下图所示
假如,现在要在B节点和D节点中间插入一个元素,只需要把B节点指向D节点的地址断掉,重新指向新的节点地址就可以了。如下图所示:
假如,现在想要把D节点删除,只需要让C节点指向E节点的地址,然后把D节点指向E节点的地址断掉。此时D节点就会变成垃圾,会把垃圾回收器清理掉。
上面的链表是单向链表,它的方向是从头节点指向尾节点的,只能从左往右查找元素,这样查询效率比较慢;还有一种链表叫做双向链表,不光可以从做往右找,还可以从右往左找。如下图所示:
LinkedList集合是基于双向链表实现了,所以相对于ArrayList新增了一些可以针对头尾进行操作的方法,如下图示所示:
4.5 LinkedList集合的应用场景
LinkedList集合有什么用呢?可以用它来设计栈结构、队列结构 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 LinkedList<String> queue = new LinkedList <>(); queue.addLast("第1号人" ); queue.addLast("第2号人" ); queue.addLast("第3号人" ); queue.addLast("第4号人" ); System.out.println(queue); System.out.println(queue.removeFirst()); System.out.println(queue.removeFirst()); System.out.println(queue.removeFirst()); System.out.println(queue.removeFirst());
接着,我们就用LinkedList来模拟下栈结构,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 LinkedList<String> stack = new ArrayList <>(); stack.push("第1颗子弹" ); stack.push("第2颗子弹" ); stack.push("第3颗子弹" ); stack.push("第4颗子弹" ); System.out.println(stack); System.out.println(statck.pop()); System.out.println(statck.pop()); System.out.println(statck.pop()); System.out.println(statck.pop()); System.out.println(list);
五、Set系列集合
5.1 认识Set集合的特点
Set集合是属于Collection体系下的另一个分支,它的特点如下图所示
下面我们用代码简单演示一下,每一种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);
5.2 HashSet集合底层原理
HashSet集合底层是基于哈希表
实现的,哈希表根据JDK版本的不同,也是有点区别的
JDK8以前:哈希表 = 数组+链表
JDK8以后:哈希表 = 数组+链表+红黑树
我们发现往HashSet集合中存储元素时,底层调用了元素的两个方法:一个是hashCode方法获取元素的hashCode值(哈希值);另一个是调用了元素的equals方法,用来比较新添加的元素和集合中已有的元素是否相同。
只有新添加元素的hashCode值和集合中以后元素的hashCode值相同、新添加的元素调用equals方法和集合中已有元素比较结果为true, 才认为元素重复。
如果hashCode值相同,equals比较不同,则以链表的形式连接在数组的同一个索引为位置(如上图所示)
在JDK8开始后,为了提高性能,当链表的长度超过8时,就会把链表转换为红黑树,如下图所示:
5.3 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 }
5.4 LinkedHashSet底层原理
LinkedHashSet它底层采用的是也是哈希表结构,只不过额外新增了一个双向链表
来维护元素的存取顺序。如下下图所示:
每次添加元素,就和上一个元素用双向链表连接一下。第一个添加的元素是双向链表的头节点,最后一个添加的元素是双向链表的尾节点。
把上个案例中的集合改成LinkedHashSet集合,我们观察效果怎样
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 LinkedHashSet <>(); 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=20 , height=169.6 } Student{name='蜘蛛精' , age=23 , height=169.6 } Student{name='牛魔王' , age=48 , height=169.6 }
5.5 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比较器对象
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);
5.6 总结Collection集合
5.7 并发修改异常
补充说明一下,那就是在使用迭代器遍历集合时,可能存在并发修改异常。
我们先把这个异常用代码演示出来,再解释一下为什么会有这个异常产生
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 List<String> list = new ArrayList <>(); list.add("王麻子" ); list.add("小李子" ); list.add("李爱花" ); list.add("张全蛋" ); list.add("晓李" ); list.add("李玉刚" ); System.out.println(list); Iterator<String> it = list.iterator(); while (it.hasNext()){ String name = it.next(); if (name.contains("李" )){ list.remove(name); } } System.out.println(list);
运行上面的代码,会出现下面的异常。这就是并发修改异常
为什么会出现这个异常呢?那是因为迭代器遍历机制,规定迭代器遍历集合的同时,不允许集合自己去增删元素,否则就会出现这个异常。
怎么解决这个问题呢?不使用集合的删除方法,而是使用迭代器的删除方法,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 List<String> list = new ArrayList <>(); list.add("王麻子" ); list.add("小李子" ); list.add("李爱花" ); list.add("张全蛋" ); list.add("晓李" ); list.add("李玉刚" ); System.out.println(list); Iterator<String> it = list.iterator(); while (it.hasNext()){ String name = it.next(); if (name.contains("李" )){ it.remove(); } } System.out.println(list);
六、Collection的其他操作
为了更加方便的对Collection集合进行操作,今天我们还要学习一个操作Collection集合的工具类,叫做Collections。但是Collections工具类中需要用到一个没有学过的小知识点,叫做可变参数,所以必须先学习这个前置知识可变参数,再学习Collections工具类。
6.1 可变参数
关于可变参数我们首先要知道它是什么,然后要知道它的本质。搞清楚这两个问题,可变参数就算你学明白了。
接下来,我们编写代码来演示一下
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("----------------" ); } }
最后还有一些错误写法,需要让大家写代码时注意一下,不要这么写哦!!!
6.2 Collections工具类
有了可变参数的基础,我们再学习Collections这个工具类就好理解了,因为这个工具类的方法中会用到可变参数。
注意Collections并不是集合,它比Collection多了一个s,一般后缀为s的类很多都是工具类。这里的Collections是用来操作Collection的工具类。它提供了一些好用的静态方法,如下
我们把这些方法用代码来演示一下:
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);
七、Map集合
7.1 Map概述体系
首先我们还是先认识一下什么是双列集合。
所谓双列集合,就是说集合中的元素是一对一对的。Map集合中的每一个元素是以key=value
的形式存在的,一个key=value
就称之为一个键值对,而且在Java中有一个类叫Entry类,Entry的对象用来表示键值对对象。
所有的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集合也有很多种,在Java中使用不同的类来表示的,每一种Map集合其键的特点是有些差异的,值是键的一个附属值,所以我们只关注键的特点就可以了。
7.2 Map集合的常用方法
由于Map是所有双列集合的父接口,所以我们只需要学习Map接口中每一个方法是什么含义,那么所有的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); } }
7.2.1 Map集合遍历方式1
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 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); } } }
7.2.2 Map集合遍历方式2
Map集合的第二种遍历方式,这种遍历方式更加符合面向对象的思维。
前面我们给大家介绍过,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); } } }
7.2.3 Map集合遍历方式3
Map集合的第三种遍历方式,需要用到下面的一个方法forEach,而这个方法是JDK8版本以后才有的。调用起来非常简单,最好是结合的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 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); }); } }
7.3 HashMap
HashMap集合的底层原理。前面我们学习过HashSet的底层原理,实际上HashMap底层原理和HashSet是一样的。为什么这么说呢?因为我们往HashSet集合中添加元素时,实际上是把元素作为添加添加到了HashMap集合中。
下面是Map集合的体系结构,HashMap集合的特点是由键决定的: 它的键是无序、不能重复,而且没有索引的 。再各种Map集合中也是用得最多的一种集合。
刚才我们说,HashSet底层就是HashMap,我们可以看源码验证这一点,如下图所示,我们可以看到,创建HashSet集合时,底层帮你创建了HashMap集合;往HashSet集合中添加添加元素时,底层却是调用了Map集合的put方法把元素作为了键来存储。所以实际上根本没有什么HashSet集合,把HashMap的集合的值忽略不看就是HashSet集合。
HashSet的原理我们之前已经学过了,所以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); } }
上面存储的键,有两个蜘蛛精,但是打印出只会有最后一个。
7.4 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集合键存储和取出的顺序是一样的
1 {手表=220 ,手机=2 ,Java=2 ,null =null }
如果是HashMap,键存储和取出的顺序是不一致的
1 {null =null ,手机=2 ,手表=220 ,Java=2 ,}
LinkedHashMap的底层原理,和LinkedHashSet底层原理是一样的。底层多个一个双向链表来维护键的存储顺序。
取元素时,先取头节点元素,然后再依次取下一个几点,一直到尾结点。所以是有序的。
7.5 TreeMap
TreeMap集合的特点也是由键决定的,默认按照键的升序排列,键不重复,也是无索引的。
**排序方式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键不能排序。
Stream流、File类
一、JDK8新特性(Stream流)
1.1 Stream流体验
案例需求:有一个List集合,元素有"张三丰","张无忌","周芷若","赵敏","张强"
,找出姓张,且是3个字的名字,存入到一个新集合中去。
1 2 3 List<String> names = new ArrayList <>(); Collections.addAll(names, "张三丰" ,"张无忌" ,"周芷若" ,"赵敏" ,"张强" ); System.out.println(names);
1 2 3 4 5 6 7 8 List<String> list = new ArrayList <>(); for (String name : names) { if (name.startsWith("张" ) && name.length() == 3 ){ list.add(name); } } System.out.println(list);
用Stream流来做,代码是这样的(ps: 是不是想流水线一样,一句话就写完了)
1 2 List<String> list2 = names.stream().filter(s -> s.startsWith("张" )).filter(a -> a.length()==3 ).collect(Collectors.toList()); System.out.println(list2);
学习Stream流我们接下来,会按照下面的步骤来学习。
1.2 Stream流的创建
如何创建Stream流、或者叫获取Stream流。
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); } }
1.3 Stream流中间方法
Stream流中间操作的方法。
中间方法指的是:调用完方法之后其结果是一个新的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 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); } }
1.4 Stream流终结方法
Stream流的终结方法。这些方法的特点是,调用完方法之后,其结果就不再是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 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类
**但是需要我们注意:**File对象只能对文件进行操作,不能操作文件中的内容。
2.1 File对象的创建
学习File类和其他类一样,第一步是创建File类的对象。 想要创建对象,我们得看File类有哪些构造方法。
下面我们演示一下,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()); } }
2.2 File判断和获取方法
File对象封装的路径是存在还是不存在,是文件还是文件夹其实是不清楚的。好在File类提供了方法可以帮我们做判断。
话不多少,直接上代码
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.3 创建和删除方法
File类提供了创建和删除文件的方法,话不多少,看代码。
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(): 文件可以直接删除,但是文件夹只能删除空的文件夹,文件夹有内容删除不了。
2.4 遍历文件夹方法
话不多少上代码,演示一下
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
2.5 递归文件搜索
案例需求:在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); } } } } }
字符集、IO流(一)
一、字符集
1.1 字符集的来历
我们知道计算机是美国人发明的,由于计算机能够处理的数据只能是0和1组成的二进制数据,为了让计算机能够处理字符,于是美国人就把他们会用到的每一个字符进行了编码(所谓编码,就是为一个字符编一个二进制数据 ),如下图所示:
美国人常用的字符有英文字母、标点符号、数字以及一些特殊字符,这些字符一共也不到128个,所以他们用1个字节来存储1字符就够了。 美国人把他们用到的字符和字符对应的编码总结成了一张码表,这张码表叫做ASCII码表(也叫ASCII字符集)。
其实计算机只在美国用是没有问题的,但是计算机慢慢的普及到全世界,当普及到中国的时候,在计算机中想要存储中文,那ASCII字符集就不够用了,因为中文太多了,随便数一数也有几万个字符。
于是中国人为了在计算机中存储中文,也编了一个中国人用的字符集叫做GBK字符集,这里面包含2万多个汉字字符,GBK中一个汉字采用两个字节来存储 ,为了能够显示英文字母,GBK字符集也兼容了ASCII字符集,在GBK字符集中一个字母还是采用一个字节来存储 。
1.2 汉字和字母的编码特点
需要我们注意汉字和字母的编码特点:
如果是存储字母,采用1个字节来存储,一共8位,其中第1位是0
如果是存储汉字,采用2个字节来存储,一共16位,其中第1位是1
当读取文件中的字符时,通过识别读取到的第1位是0还是1来判断是字母还是汉字
如果读取到第1位是0,就认为是一个字母,此时往后读1个字节。
如果读取到第1位是1,就认为是一个汉字,此时往后读2个字节。
1.3 Unicode字符集
为了解决各个国家字符集互不兼容的问题,由国际化标准组织牵头,设计了一套全世界通用的字符集,叫做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.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 个字节
1.5 编码和解码
搞清楚字符集的知识之后,我们接下来再带着同学们使用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流(字节流)
2.1 IO流概述
IO流的作用:就是可以对文件或者网络中的数据进行读、写的操作。如下图所示
把数据从磁盘、网络中读取到程序中来,用到的是输入流。
把程序中的数据写入磁盘、网络中,用到的是输出流。
简单记:输入流(读数据)、输出流(写数据)
IO流在Java中有很多种,不同的流来干不同的事情。Java把各种流用不同的类来表示,这些流的继承体系如下图所示:
1 2 3 IO流分为两大派系: 1. 字节流:字节流又分为字节输入流、字节输出流 2. 字符流:字符流由分为字符输入流、字符输出流
字节流中的字节输入流,用InputStream来表示。但是InputStream是抽象类,我们用的是它的子类,叫FileInputStream。
需要用到的方法如下图所示:有构造方法、成员方法
使用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个汉字,此时将这个字节转换为字符,是会有乱码的。
为了提高效率,我们可以使用另一个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
还需要注意一个问题:采用一次读取多个字节的方式,也是可能有乱码的。因为也有可能读取到半个汉字的情况。
我们可以一次性读取文件中的全部字节,然后把全部字节转换为一个字符串,就不会有乱码了。
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();
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();
最后,还是要注意一个问题:一次读取所有字节虽然可以解决乱码问题,但是文件不能过大,如果文件过大,可能导致内存溢出。
2.5 FileOutputStream写字节
往文件中写数据需要用到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(); } }
2.6 字节流复制文件
比如:我们要复制一张图片,从磁盘D:/resource/meinv.webp
的一个位置,复制到C:/data/meinv.webp
位置。
复制文件的思路如下图所示:
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.webp" ); OutputStream os = new FileOutputStream ("C:/data/meinv.webp" ); 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以后分别给出了不同的处理方案。
3.1 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之后代码多了十几行,而且阅读性并不高。难受…
3.2 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.webp" ); OutputStream os = new FileOutputStream ("C:/data/meinv.webp" ); ){ 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(); } } }
IO流(二)
一、字符流
1.1 FileReader类
先类学习字符流中的FileReader类,这是字符输入流,用来将文件中的字符数据读取到程序中来。
FileReader读取文件的步骤如下:
1 2 3 第一步:创建FileReader对象与要读取的源文件接通 第二步:调用read()方法读取文件中的字符 第三步:调用close()方法关闭流
需要用到的方法:先通过构造器创建对象,再通过read方法读取数据(注意:两个read方法的返回值,含义不一样 )
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(); } } }
1.2 FileWriter类
FileWriter,它可以将程序中的字符数据写入文件。
FileWriter往文件中写字符数据的步骤如下:
1 2 3 第一步:创建FileWirter对象与要读取的目标文件接通 第二步:调用write(字符数据/字符数组/字符串)方法读取文件中的字符 第三步:调用close()方法关闭流
需要用到的方法如下:构造器是用来创建FileWriter对象的,有了对象才能调用write方法写数据到文件。
接下来,用代码演示一下:
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(); } } }
1.3 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();
但是需要注意的是,关闭流之后,就不能在对流进行操作了。否则会出异常
二、缓冲流
缓冲流有四种,如下图所示
缓冲流的作用 :可以对原始流进行包装,提高原始流读写数据的性能。
2.1 缓冲字节流
我们先来学习字节缓冲流是如何提高读写数据的性能的,原理如下图所示。是因为在缓冲流的底层自己封装了一个长度为8KB(8129byte)的字节数组,但是缓冲流不能单独使用,它需要依赖于原始流。
**读数据时:**它先用原始字节输入流一次性读取8KB的数据存入缓冲流内部的数组中(ps: 先一次多囤点货),再从8KB的字节数组中读取一个字节或者多个字节(把消耗屯的货)。
写数据时: 它是先把数据写到缓冲流内部的8BK的数组中(ps: 先攒一车货),等数组存满了,再通过原始的字节输出流,一次性写到目标文件中去(把囤好的货,一次性运走)。
在创建缓冲字节流对象时,需要封装一个原始流对象进来。构造方法如下
如果我们用缓冲流复制文件,代码写法如下:
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.
而且BufferedReader还要特有的方法,一次可以读取文本文件中的一行
使用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。
而且BufferedWriter新增了一个功能,可以用来写一个换行符
接下来,用代码演示一下,使用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(); } } }
2.3 缓冲流性能分析
我们说缓冲流内部多了一个数组,可以提高原始流的读写性能。讲到这一定有同学有这么一个疑问,它和我们使用原始流,自己加一个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 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(); } } }
执行完之后,你会发现没有乱码。
3.2 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(); } } }
四、打印流
4.1 打印流基本使用
打印流,这里所说的打印其实就是写数据的意思,它和普通的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(); } } }
4.2 重定向输出语句
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.
5.1 DataOutputStream类
我们先学习DataOutputStream类,它也是一种包装流,创建DataOutputStream对象时,底层需要依赖于一个原始的OutputStream流对象。然后调用它的wirteXxx方法,写的是特定类型的数据。
代码如下:往文件中写整数、小数、布尔类型数据、字符串数据
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()方法就可以读取特定类型的数据。
代码如下:读取文件中特定类型的数据(整数、小数、字符串等)
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 序列化:意思就是把对象写到文件或者网络中去。(简单记:写对象) 反序列化:意思就是把对象从文件或者网络中读取出来。(简单记:读对象)
6.1 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操作,由apache开源基金组织提供了一组有关IO流小框架,可以提高IO流的开发效率。
这个框架的名字叫commons-io:其本质是别人写好的一些字节码文件(class文件),打包成了一个jar包。我们只需要把jar包引入到我们的项目中,就可以直接用了。
这里给同学们介绍一个jar包中提供的工具类叫FileUtils,它的部分功能如下,很方便,你一看名字就知道怎么用了。
在写代码之前,先需要引入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" ))); } }
特殊文件、日志技术
一、属性文件
1.1 特殊文件概述
在以后的Java开发过程中还会遇到一些特殊的文本文件,这些文件是有一些格式要求的,方便程序对文件中的数据进行处理。
比如,后面我们会用到两种特殊的文本文件,一种是properties文件,还有一种是xml文件。如下图所示。
后缀为.properties的文件,称之为属性文件,它可以很方便的存储一些类似于键值对的数据。经常当做软件的配置文件使用。
而xml文件能够表示更加复杂的数据关系,比如要表示多个用户的用户名、密码、家乡、性别等。在后面,也经常当做软件的配置文件使用。
1.2 Properties属性文件
接下来,我们先学习Properties这种属性文件。首先我们要掌握属性文件的格式:
属性文件后缀以.properties
结尾
属性文件里面的每一行都是一个键值对,键和值中间用=隔开。比如: admin=123456
#
表示这样是注释信息,是用来解释这一行配置是什么意思。
每一行末尾不要习惯性加分号,以及空格等字符;不然会把分号,空格会当做值的一部分。
键不能重复,值可以重复
如下图所示
接下来,我们学习如何读取属性文件中的数据。这里需要给同学们,介绍一个来叫Properties.
1 2 3 4 5 1. Properties是什么? Properties是Map接口下面的一个实现类,所以Properties也是一种双列集合,用来存储键值对。 但是一般不会把它当做集合来使用。 2. Properties核心作用? Properties类的对象,用来表示属性文件,可以用来读取属性文件中的键值对。
使用Properties读取属性文件中的键值对 ,需要用到的方法如下。
实用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往属性文件中写键值对 ,需要用到的方法如下
往Properties属性文件中写键值对的步骤如下
1 2 3 4 5 1 、先准备一个.properties属性文件,按照格式写几个键值对1 、创建Properties对象出来,2 、调用setProperty存储一些键值对数据3 、调用store(字符输出流/字节输出流, 注释),将Properties集合中的键和值写到文件中 注意:第二个参数是注释,必须得加;
先准备一个users.properties
属性文件,如下图所示
接下来,编写代码读取上面的属性文件。代码如下:
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!" ); } }
运行上面的代码,user2.properties
配置文件打开效果如下图所示。
二、XML文件
2.1 XML文件概述
首先,我们来认识一下,什么是XML?
XML是可扩展的标记语言,意思是它是由一些标签组成的,而这些标签是自己定义的。本质上一种数据格式,可以用来表示复杂的数据关系。
XML文件有如下的特点:
XML中的<标签名>
称为一个标签或者一个元素,一般是成对出现的。
XML中的标签名可以自己定义(可扩展),但是必须要正确的嵌套
XML中只能有一个根标签。
XML标准中可以有属性
XML必须第一行有一个文档声明,格式是固定的<?xml version="1.0" encoding="UTF-8"?>
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在实际开发中有什么作用?
2.2 XML解析1
使用程序读取XML文件中的数据,称之为XML解析。这里并不需要我们自己写IO流代码去读取xml文件中的数据。其实有很多开源的,好用的XML解析框架,最知名的是DOM4J(第三方开发的)
由于DOM4J是第三方提供的,所以需要把第三方提供的Jar包导入到自己的项目中来,才可以使用。具体步骤如下:
DOM4J解析XML文件的思想是:文档对象模型(意思是把整个XML文档、每一个标签、每一个属性都等都当做对象来看待)。Dowument对象表示真个XML文档、Element对象表示标签(元素)、Attribute对象表示属性、标签中的内容就是文本
DOM4J解析XML需要用到的方法如下图所示
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()); } }
2.3 XML解析2
获取到XML文件的根元素之后,接下来,就可以用根元素在获取到它里面的子元素(包括子标签、表属性等)。需要用到的方法如下图所示
接下来,把上面的方法先一个一个的演示一下。
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()); } }
2.4 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(); } } }
2.5 XML约束(了解)
各位小伙伴,关于XML还有最后一个知识需要大家了解一下。这个知识叫做约束XML文件的编写,我讲这个知识的目的是因为同学们以后在开发过程中会遇到这个知识,但是这个代码不需要大家写,了解一下就可以了。
首先,说一些什么是XML约束?
XML约束指的是限制XML文件中的标签或者属性,只能按照规定的格式写。
比如我在项目中,想约束一个XML文件中的标签只能写<书>、<书名>、<作者>、<售价>这几个标签,如果写其他标签就报错。
三、日志技术
3.1 日志概述
想搞清楚什么是日志,其实可以通过下面几个问题来了解的。
系统系统能记住某些数据被谁操作,比如被谁删除了?
想分析用户浏览系统的具体情况,比如挖掘用户的具体喜好?
当系统在开发中或者上线后出现了Bug,崩溃了,该通过什么去分析,定位Bug?
而日志就可以帮我们解决以上的问题。所以日志就好比生活中的日记,日记可以记录生活中的点点滴滴;而程序中的日志,通常就是一个文件,里面记录了程序运行过程中产生的各种数据。
日志技术有如下好处
日志可以将系统执行的信息,方便的记录到指定位置,可以是控制台、可以是文件、可以是数据库中。
日志可以随时以开关的形式控制启停,无需侵入到源代码中去修改。
3.2 日志的体系
大家注意了在行内,其实有很多日志框架给开发者使用。所谓日志框架就是由一些牛人或者第三方公司已经做好的实现代码,后来者就可以直接拿过去使用。
日志框架有很多种,比如有JUL(java.util.logging)、Log4j、logback等。但是这些日志框架如果使用的API方法都不一样的话,使用者的学习成本就很高。为了降低程序员的学习压力,行内提供了一套日志接口,然后所有的日志框架都按照日志接口的API来实现就可以了。
这样程序员只要会一套日志框架,那么其他的也就可以通过用,甚至可以在多套日志框架之间来回切换。比较常用的日志框架,和日志接口的关系如下图所示
这里推荐使用Logback日志框架,也在行业中最为广泛使用的。
Logback日志分为哪几个模块
3.3 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
看一下文件中是否有记录日志吧!!
3.4 日志配置文件
Logback提供了一个核心配置文件logback.xml,日志框架在记录日志时会读取配置文件中的配置信息,从而记录日志的形式。具体可以做哪些配置呢?
1 2 3 1. 可以配置日志输出的位置是文件、还是控制台2. 可以配置日志输出的格式3. 还可以配置日志关闭和开启、以及哪些日志输出哪些日志不输出。
如下图所示,控制日志往文件中输出,还是往控制台输出
3.5 配置日志级别
多线程
一、多线程
线程其实是程序中的一条执行路径。
我们之前写过的程序,其实都是单线程程序,如下图代码,如果前面的for循环没有执行完,for循环下面的代码是不会执行的。
怎样的程序才是多线程程序呢? 如下图所示,12306网站就是支持多线程的,因为同时可以有很多人一起进入网站购票,而且每一个人互不影响。再比如百度网盘,可以同时下载或者上传多个文件。这些程序中其实就有多条执行路径,每一条执行执行路径就是一条线程,所以这样的程序就是多线程程序。
1.1 线程创建方式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方法中的执行的代码会成为主线程的一部分。此时执行结果是这样的。
1.2 线程创建方式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
1.3 线程创建方式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); } } }
1.4 线程的创建方式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); } } }
执行上面代码,效果如下图所示,我们发现每一条线程都有自己了名字了。
最后再演示一下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号线程执行完就结束了。
我们再尝试,把join()方法去掉,再看执行效果。此时你会发现2号线程没有执行完1号线程就执行了**(效果是多次运行才出现的,根据个人电脑而异,可能有同学半天也出现不了也是正常的)**
三、线程安全问题
3.1 线程安全问题概述
线程安全问题指的是,多个线程同时操作同一个共享资源的时候,可能会出现业务安全问题。
下面通过一个取钱的案例给同学们演示一下。案例需求如下
1 场景:小明和小红是一对夫妻,他们有一个共享账户,余额是10 万元,小红和小明同时来取钱,并且2 人各自都在取钱10 万元,可能出现什么问题呢?
如下图所示,小明和小红假设都是一个线程,本类每个线程都应该执行完三步操作,才算是完成的取钱的操作。但是真实执行过程可能是下面这样子的
① 小红线程只执行了判断余额是否足够(条件为true),然后CPU的执行权就被小红线程抢走了。
② 小红线程也执行了判断了余额是否足够(条件也是true), 然后CPU执行权又被小明线程抢走了。
③ 小明线程由于刚才已经判断余额是否足够了,直接执行第2步,吐出了10万元钱,此时共享账户月为0。然后CPU执行权又被小红线程抢走。
④ 小红线程由于刚刚也已经判断余额是否足够了,直接执行第2步,吐出了10万元钱,此时共享账户月为-10万。
你会发现,在这个取钱案例中,两个人把共享账户的钱都取了10万,但问题是只有10万块钱啊!!!
以上取钱案例中的问题,就是线程安全问题的一种体现。
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 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完了。
3.3 线程同步方案
为了解决前面的线程安全问题,我们可以使用线程同步思想。同步最常见的方案就是加锁,意思是每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动释放锁,然后其他线程才能再加锁进来。
等小红线程执行完了,把余额改为0,出去了就会释放锁。这时小明线程就可以加锁进来执行,如下图所示。
采用加锁的方案,就可以解决前面两个线程都取10万块钱的问题。怎么加锁呢?Java提供了三种方案
3.4 同步代码块
我们先来学习同步代码块。它的作用就是把访问共享数据的代码锁起来,以此保证线程安全。
使用同步代码块,来解决前面代码里面的线程安全问题。我们只需要修改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)当做锁对象
3.5 同步方法
其实同步方法,就是把整个方法给锁住,一个线程调用这个方法,另一个线程调用的时候就执行不了,只有等上一个线程调用结束,下一个线程调用才能继续执行。
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. 同步代码块是将方法中的部分代码锁住
3.6 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做的肉包子
五、线程池
5.1 线程池概述
线程池就是一个可以复用线程的技术 。
要理解什么是线程复用技术,我们先得看一下不使用线程池会有什么问题
1 假设:用户每次发起一个请求给后台,后台就创建一个新的线程来处理,下次新的任务过来肯定也会创建新的线程,如果用户量非常大,创建的线程也讲越来越多。然而,创建线程是开销很大的,并且请求过多时,会严重影响系统性能。
而使用线程池,就可以解决上面的问题。如下图所示,线程池内部会有一个容器,存储几个核心线程,假设有3个核心线程,这3个核心线程可以处理3个任务。
但是任务总有被执行完的时候,假设第1个线程的任务执行完了,那么第1个线程就空闲下来了,有新的任务时,空闲下来的第1个线程可以去执行其他任务。依此内推,这3个线程可以不断的复用,也可以执行很多个任务。
所以,线程池就是一个线程复用技术,它可以提高线程的利用率。
5.2 创建线程池
在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 核心线程和临时线程都在忙、任务队列也满了、新任务过来时才会开始拒绝任务。
5.3 线程池执行Runnable任务
创建好线程池之后,接下来我们就可以使用线程池执行任务了。线程池执行的任务可以有两种,一种是Runnable任务;一种是callable任务。下面的execute方法可以用来执行Runnable任务。
先准备一个线程任务类
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);
执行上面的代码,结果输出如下
5.4 线程池执行Callable任务
接下来,我们学习使用线程池执行Callable任务。callable任务相对于Runnable任务来说,就是多了一个返回值。
执行Callable任务需要用到下面的submit方法
先准备一个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()); } }
执行后,结果如下图所示
5.5 线程池工具类(Executors)
Java为开发者提供了一个创建线程池的工具类,叫做Executors,它提供了方法可以创建各种不能特点的线程池。如下图所示
接下来,我们演示一下创建固定线程数量的线程池。这几个方法用得不多,所以这里不做过多演示,同学们了解一下就行了。
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开发手册》提供的强制规范要求。
六、补充知识(理解即可)
6.1 并发和并行
先学习第一个补充知识点,并发和并行。在讲解并发和并行的含义之前,我们先来了解一下什么是进程、线程?
正常运行的程序(软件)就是一个独立的进程
线程是属于进程,一个进程中包含多个线程
进程中的线程其实并发和并行同时存在(继续往下看)
我们可以打开系统的任务管理器看看(快捷键:Ctrl+Shfit+Esc),自己的电脑上目前有哪些进程。
知道了什么是进程和线程之后,接着我们再来学习并发和并行的含义。
首先,来学习一下什么是并发?
进程中的线程由CPU负责调度执行,但是CPU同时处理线程的数量是优先的,为了保证全部线程都能执行到,CPU采用轮询机制为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。(简单记:并发就是多条线程交替执行)
接下,再来学习一下什么是并行?
并行指的是,多个线程同时被CPU调度执行。如下图所示,多个CPU核心在执行多条线程
最后一个问题,多线程到底是并发还是并行呢?
其实多个线程在我们的电脑上执行,并发和并行是同时存在的。
6.2 线程的生命周期
接下来,我们学习最后一个有关线程的知识点,叫做线程的生命周期。所谓生命周期就是线程从生到死的过程中间有哪些状态,以及这些状态之间是怎么切换的。
为了让大家同好的理解线程的生命周期,先用人的生命周期举个例子,人从生到死有下面的几个过程。在人的生命周期过程中,各种状态之间可能会有切换,线程也是一样的。
接下来就来学习线程的生命周期。在Thread类中有一个嵌套的枚举类叫Thread.Status,这里面定义了线程的6中状态。如下图所示
1 2 3 4 5 6 NEW: 新建状态,线程还没有启动 RUNNABLE: 可以运行状态,线程调用了start()方法后处于这个状态 BLOCKED: 锁阻塞状态,没有获取到锁处于这个状态 WAITING: 无限等待状态,线程执行时被调用了wait方法处于这个状态 TIMED_WAITING: 计时等待状态,线程执行时被调用了sleep(毫秒)或者wait(毫秒)方法处于这个状态 TERMINATED: 终止状态, 线程执行完毕或者遇到异常时,处于这个状态。
这几种状态之间切换关系如下图所示
网络编程
一、网络编程概述
网络通信的基本架构。通信的基本架构主要有两种形式:一种是CS架构(Client 客户端/Server服务端)、一种是BS架构(Brower 浏览器/Server服务端)。
**CS架构的特点:**CS架构需要用户在自己的电脑或者手机上安装客户端软件,然后由客户端软件通过网络连接服务器程序,由服务器把数据发给客户端,客户端就可以在页面上看到各种数据了。
这两种结构不管是CS、还是BS都是需要用到网络编程的相关技术。我们学习Java的程序员,以后从事的工作方向主要还是BS架构的。
二、网络编程三要素
有哪三要素呢?分别是IP地址、端口号、通信协议
IP地址:表示设备在网络中的地址,是网络中设备的唯一标识
端口号:应用程序在设备中唯一的标识
协议:连接和数据在网络中传输的规则。
如下图所示:假设现在要从一台电脑中的微信上,发一句“你愁啥?”到其他电脑的微信上,流程如下
1 2 3 1. 先通过ip地址找到对方的电脑2. 再通过端口号找到对方的电脑上的应用程序3. 按照双方约定好的规则发送、接收数据
2.1 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地址相关的一个东西,叫做域名。
域名和IP其实是一一对应的,由运营商来管理域名和IP的对应关系。我们在浏览器上敲一个域名时,首先由运营商的域名解析服务,把域名转换为ip地址,再通过IP地址去访问对应的服务器设备。
关于IP地址,还有一个特殊的地址需要我们记住一下。就是我们在学习阶段进行测试时,经常会自己给自己消息,需要用到一个本地回送地址:127.0.0.1
最后给同学们介绍,两个和IP地址相关的命令
1 2 ipconfig: 查看本机的ip地址 pring 域名/ip 检测当前电脑与指定的ip是否连通
ping命令出现以下的提示,说明网络是通过的
2.2 InetAddress类
按照面向对象的设计思想,Java中也有一个类用来表IP地址,这个类是InetAddress类。我们在开发网络通信程序的时候,可能有时候会获取本机的IP地址,以及测试与其他地址是否连通,这个时候就可以使用InetAddress类来完成。下面学习几个InetAddress的方法。
演示上面几个方法的效果
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 )); } }
2.3 端口号
端口号:指的是计算机设备上运行的应用程序的标识,被规定为一个16位的二进制数据,范围(0~65535)
端口号分为一下几类(了解一下)
周知端口:0~1023,被预先定义的知名应用程序占用(如:HTTP占用80,FTP占用21)
注册端口:1024~49151,分配给用户经常或者某些应用程序
动态端口:49152~65536,之所以称为动态端口,是因为它一般不固定分配给某进程,而是动态分配的。
需要我们注意的是,同一个计算机设备中,不能出现两个应用程序,用同一个端口号
2.4 协议
各位同学,前面我们已经学习了IP地址和端口号,但是想要完成数据通信还需要有通信协议。
网络上通信的设备,事先规定的连接规则,以及传输数据的规则被称为网络通信协议。
为了让世界上各种上网设备能够互联互通,肯定需要有一个组织出来,指定一个规则,大家都遵守这个规则,才能进行数据通信。
只要按照OSI网络参考模型制造的设备,就可以在国际互联网上互联互通。其中传输层有两个协议,是我们今天会接触到的(UDP协议、TCP协议)
三次握手如下图所示**:目的是确认通信双方,手法消息都是正常没问题的**
四次挥手如下图所示:目的是确保双方数据的收发已经完成,没有数据丢失
三、UDP通信代码(入门案例)
UDP是面向无连接的、不需要确认双方是否存在,所以它是不可靠的协议。Java提供了一个类叫DatagramSocket来完成基于UDP协议的收发数据。使用DatagramSocket收发数据时,数据要以数据包的形式体现,一个数据包限制在64KB以内
下面我们看一个案例,需要有两个程序,一个表示客户端程序,一个表示服务端程序。
需求:客户端程序发一个字符串数据给服务端,服务端程序接收数据并打印。
3.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 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(); } }
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 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时客户端程序退出。
4.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 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); } } }
4.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 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获取到一个字节输出流,将数据写给客户端
客户端这边再获取输入流,通过字节输入流来读取服务端写过来的数据。
5.1 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(); } }
5.2 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(); } } }
6.2 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开启一条线程取执行读取数据的操作,来多少个客户端,就有多少条线程。按照这样的设计,服务端就可以支持多个客户端连接了。
按照上面的思路,改写服务端代码。
7.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 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(); } } }
7.2 案例拓展(群聊)
接着前面的案例,下面我们案例再次拓展一下,这个并不需要同学们必须掌握,主要是为了锻炼同学们的编程能力、和编程思维。
我们想把刚才的案例,改进成全能够实现群聊的效果,就是一个客户端发的消息,其他的每一个客户端都可以收到。
刚才我们写的多个客户端可以往服务端发现消息,但是客户端和客户端是不能直接通信的。想要试下全群聊的效果,我们还是必须要有服务端在中间做中转。 具体实现方案如下图所示:
我们可以在服务端创建一个存储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协议规定数据格式)如下图所示:左图是数据格式,右图是示例。
接下来,我们写一个服务端程序按照右图示例的样子,给浏览器返回数据。注意:数据是由多行组成的,必须按照规定的格式来写。
8.1 服务端程序
先写一个线程类,用于按照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(); } } }
8.2 服务端主程序用线程池改进
为了避免服务端创建太多的线程,可以把服务端用线程池改进,提高服务端的性能。
先写一个给浏览器响应数据的线程任务
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)); } } }