*
一. java 的基础
1.1. 方法的重载(OverLoad)
-
重载与重写的区别
- 重写、覆盖、覆写(Override):在继承关系中,方法名称一样,参数列表【也一样】
- 重载:方法名称一样,参数列表【不一样】
-
多个方法名称一样,但是参数的列表不一样
-
方法重载的注意事项,下列是与方法重载有关的
- 参数的个数不同
- 参数类型不同
- 参数的多类型顺序不同
-
下列与方法重载无关 错误的方法重载
- 参数的名称
- 与方法的返回值无关
-
好处:只需要记住一个方法名称就可以实现类似的多个功能
sum(10, 26); public static int sum(int a, int b) { return a + b; } sum(112, 30.9); public static int sum(int a, double b) { return a + b; } sum(25.3, 10.68); public static int sum(double a, double b) { return int(a + b); } sum(12, 19, 21); public static int sum(int a, int b, int c) { return a + b + c; }
1.2. 数组
1.2.1. 数组的初始化
-
动态的初始化(指定长度)
// 格式:数据类型[] 数组名称 = new 数据类型[数组长度] int[] arrayA = new int[300]; double arrayB = new double[10]; String[] arrayC = new String[5];
-
静态的初始化(指定内容)
// 格式: 数据类型[] 数组名称 = new 数据类型[] {元素1, 元素2, 元素3} int[] arrayA = new int[] { 5, 26, 98, 17 }; String[] arrayB = new String[] { "Java", "Python", "Node", "JavaScript" }; // 省略格式 数据类型[] 数组名称 = {元素1, 元素2, 元素3} double[] arrayC = { 18.02, 10, 12.0, 59.99, 5 }
1.2.2. 数组的遍历
int[] arr = new int[] { 10, 589, 31, 20, 89 }
for(int i = 0; i < arr.length; i++) {
system.out.print(arr[i] + " ")
}
-
数组元素反转(不使用临时数组 使用临时变量)
// 法一: for (int min = 0, max = array.length - 1; min < max ; min++, max--) { int temp = array[min]; array[min] = array[max]; array[max] = temp; } // 法二: for (int i = 0; i < array.length / 2; i++) { int temp = array[i]; array[i] = array[array.length - (i + 1)]; array[array.length - (i + 1)] = temp; }
-
数组工具类
- public static String toString(数组):将参数数组变成字符串(按照默认格式:[元素1, 元素2,元素3…]
- public static void sort(数组):按照默认升序(从小到大)对数组的元素进行排序
- 如果是数值,sort 默认按升序从小到大
- 如果是字符串,sort 默认按照字母升序
- 如果是自定义的类型,那么这个自定义的类需要有 Comparable 或者Comparator 接口的支持
int[] arr = { 14, 26, 8, 9, 4, 414, 258 }; String str = Arrays.toString(arr); // [14, 26, 8, 9, 4, 414, 258] Arrays.sort(arr); // 按照升序进行排序
1.3. 面向对象
1.3.1. Random 生成随机数
Random r = new Random();
// 随机生成 [0, 10) 之间的数
int res = r.nextInt(10)
-
猜数字小游戏
Random r = new Random(); // 要猜的随机数 int num = r.nextInt(100); System.out.println("随机数是" + num); Scanner sc = new Scanner(System.in); System.out.println("请输入你猜的数:"); int guess = sc.nextInt(); int count = 5; // 有五次猜数的机会 while(true) { if(guess > num) { System.out.println(guess + "太大了!"); } else if(guess < num) { System.out.println(guess + "太小了"); } else { System.out.println("恭喜你猜对了"); break; } count--; if(count == 0) { System.out.println("你输了..."); break; } System.out.println("你还有" + count + "次机会!"); guess = sc.nextInt(); }
-
对象数组
Person[] person = new Person[2]; Person p1 = new Person("周杰伦", 40); Person p2 = new Person("古月娜", 22); person[0] = p1; person[1] = p2;
1.3.2. 数学工具类 Math
- public static double abs(double num):获取绝对值
- public static double ceil(double num):向上取整
- public static double floor(double num):向下取整
- public static long round(double num):四舍五入
Math.abs(3.14); // 3.14
Math.ceil(3.9); // 4.0
Math.floor(3.99); // 3.0
Math.round(3.49); // 3
Math.PI; // 3.141592653589793
1.3.3. 对象的向上向下转型
- 对象的向上转型,其实就是多态写法(多态的方法是编译看左,运行看右)
- 格式:父类名称 对象名 = new 子类名称(); Animal animal = new Cat();
- 含义:右侧创建一个子类对象,把它当作父类来看待使用
- 注意事项:向上转型一定是安全的,原因:从小范围转向了大范围
- 对象一旦向上转型之后就不能调用子类中原本特有的方法
- 格式:父类名称 对象名 = new 子类名称(); Animal animal = new Cat();
- 对象的向下转型:其实是还原的动作
- 格式:子类名称 对象名 = (子类名称) 父类对象
- 含义:将父类对象,【还原】成为本来的子类对象
- 注意:必须保证向上转型时是猫,向下转型(还原时)也是猫
- 含义:将父类对象,【还原】成为本来的子类对象
- 格式:子类名称 对象名 = (子类名称) 父类对象
Animal animal = new Cat();
animal.eat();
Dog dog = (Dog)animal // 会报转换类异常
Cat cat = (Cat)animal;
cat.catchMouse();
- instanceof 关键字进行类型判断
- 格式:对象名 instanceof 类型
- 返回值是 boolean
- 格式:对象名 instanceof 类型
Animal animal = new Dog();
if(animal instanceof Dog) {
Dog dog = (Dog)animal;
dog.eat(); // 狗吃骨头
}
if(animal instanceof Cat) {
Cat cat = (Cat)animal;
cat.eat();
}
1.3.4. 类作为成员变量类型
- 武器类 Weapon
public class Weapon {
private String code; // 武器的代号
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public Weapon() {
}
public Weapon(String code) {
this.code = code;
}
}
- 英雄类 Hero
public class Hero {
private String name; // 英雄的名字
private int age; // 英雄的年龄
private Weapon weapon; // 武器
public Hero() {
}
public Hero(String name, int age, Weapon weapon) {
this.name = name;
this.age = age;
this.weapon = weapon;
}
public void attack() {
System.out.println("年龄为" + age + "的" + name + "用" + weapon.getCode() + "攻击敌方");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Weapon getWeapon() {
return weapon;
}
public void setWeapon(Weapon weapon) {
this.weapon = weapon;
}
}
- 测试
// 创建英雄角色
Hero hero = new Hero();
// 设置名字
hero.setName("盖伦");
// 设置年龄
hero.setAge(20);
Weapon weapon = new Weapon("多兰剑");
// 配备武器
hero.setWeapon(weapon);
hero.attack(); // 年龄为20的盖伦用多兰剑攻击敌方
1.4. 集合
1.4.1. ArrayList 集合
-
对于 ArrayList 集合来说,add 添加动作一定是成功的,所以返回值可不用,但对于其他集合来说,add 添加动作不一定成功
-
基本使用
// <String> 泛型, 集合中存储的数据类型 ArrayList<String> list = new ArrayList<>(); // 往集合中添加元素 list.add("Java");
-
ArrayList 集合的常用方法和遍历
// 返回值是一个布尔值 boolean success = list.add("Node"); // 从集合中获取元素:get 索引从 0 开始 String lang = list.get(0); // 从集合中删除元素:remove 索引从 0 开始 list.remove(0); // 获取集合的长度 list.size(); // 遍历集合 for (int i = 0; i < list.size(); i++) { System.out.print(list.get(i) + " "); }
-
如果希望向集合中存储基本数据类型,必须使用基本类型的 包装类
-
泛型只能是引用类型,不能是基本数据类型
ArrayList<Integer> list = new ArrayList<>(); list.add(15);
-
从 JDK 1.5+ 开始,支持自动装箱、自动拆箱
- 自动装箱 基本类型 —> 包装类型
- 自动拆箱 包装类型 —> 基本类型
-
-
用集合来存储自定义学生对象
ArrayList<Student> list = new ArrayList<>(); Student one = new Student("Java", 56, "123"); Student two = new Student("JavaScript", 43, "159159"); list.add(one); list.add(two) System.out.print(list.get(1)); // JavaScript
1.5. 字符串
-
创建字符串
String str1 = new String(); // 空的 // 根据字符数组创建字符串 char[] charArray = { 'A', 'b', 'c' }; String str2 = new String(charArray); // Abc // 根据字节数组创建字符串 byte[] byteArray = { 65, 66, 67 }; String str3 = new String(byteArray); // ABC // 直接创建 String str4 = "Java"; // Java
-
字符串的常量池5
- 程序当中直接写上的双引号字符串,就在字符串常量池中
- 对于基本类型来说 == 是进行数值的比较
- 对于引用数据类型来说 == 是进行地址值的比较
String str1 = "java"; String str2 = "java"; char[] charArray = { 'j', 'a', 'v', 'a' }; String str3 = new String(charArray); System.out.println(str1 == str2); // true System.out.println(str2 == str3); // false
- 程序当中直接写上的双引号字符串,就在字符串常量池中
-
字符串的比较相关方法
// str1.equals(str2) // 比较的是内容 String str1 = "Java"; String str2 = "Java"; char[]charArray = { 'J', 'a', 'v', 'a' }; String str3 = new String(charArray); System.out.println(str1.equals(str2)); // true System.out.println(str1.equals(str3)); // true System.out.println("Java".equals(str1)); // true // 如果比较双方一个常量一个变量,推荐把常量字符串写在前面 String str5 = ""; "Java".equals(str5); // 不会报空指针异常 str5.equals("Java"); // 会报空指针异常 // equalsIgnoreCase() 比较时忽略大小写 String strA = "Java"; String stra = "java"; strA.equalsIgnoreCase(stra) // true
-
字符串的获取相关方法
- public int length():获取字符串的长度
- public String concat(String str):将当前字符串和参数字符串拼接成新的字符串,返回值:新的字符串
- public char charAt(int index):获取指定索引位置的单个字符。(索引从0开始)
- public int indexOf(String str):查找参数字符串在当前字符串中首次出现的索引位置,若没有则返回 -1
String str1 = "Java"; String str2 = "JavaScript"; String str3 = str1.concat(str2); // str1 str2 的值不会改变 System.out.println(str3); // JavaJavaScript char ch = str1.charAt(0); // J str1.indexOf("va") // 2
-
字符串的截取方法
- public String substring(int index),截取从参数位置一直到字符串末尾,返回新的字符串
- public String substring(int begin, int end),截取从 begin 开始,一直到 end 结束
- 范围:[begin, end)
String str1 = "JavaScript"; String str2 = str1.substring(5); // cript String str3 = str1.substring(2, 6); // vaSc
-
字符串的转换相关方法
- public char[] toCharArray(),将当前字符串拆分成为字符数组作为返回值
- public byte[] getBytes(),获取当前字符串底层的字节数组
- public String replace(CharSequence, oldString, CharSequence newString)
- 将所有出现的老字符串替换成为新的字符串,返回替换后的结果
- CharSequence 意思是可以接受字符串类型
String str1 = "JavaScript"; char[] charArray = str1.toCharArray(); System.out.println(charArray.length); // 10 // 转换成为字节数组 byte[] bytes = str1.getBytes(); System.out.println(bytes[1]); // 97 // Java(str1不会变,生成的是新的字符串) System.out.println(str1.replace("JavaScript", "Java"));
-
字符串的分割方法
- public String[] split(String regex),按照参数的规则,将字符串切成若干部分
- split 方法的参数是一个正则表达式
String str1 = "Java,Scr,ipt"; String[] splits = str1.split(","); for (int i = 0; i < splits.length; i++) { System.out.print(splits[i] + " "); // Java Scr ipt } // str1.split(".") 是切不成功的,str1.split("\\\.")是可以切成功的
- public String[] split(String regex),按照参数的规则,将字符串切成若干部分
-
Objects 类的 equals 方法
String a = "abc"; String b = ""; Objects.equals(b, a) // 是容忍空指针异常的
1.6. 抽象方法和抽象类
-
如果说父类中的方法不确定如何进行 {} 方法,那么这应该是一个 抽象方法
-
抽象方法和抽象类的格式
- 抽象方法:就是加上 abstract 关键字,然后去掉大括号,直接分号结束
- 抽象类:抽象方法所在的类,必须是抽象类才行,在class之前写上 abstract 即可
// 抽象类 public abstract class Animal { // 抽象方法,代表吃东西,但是具体吃什么(大括号的内容)不确定 public abstract void eat(); } // 普通的成员方法(抽象类中也可有成员方法) public void getName(){}
-
-
抽象方法和抽象类的注意事项和使用
-
不能直接创建 new 抽象类对象(没有抽象方法的抽象类,也不能直接创建对象,在一些特殊场景下有用途)
-
可以有构造方法,是提供子类创建对象时,初始化父类成员使用的
-
抽象类中,不一定包含抽象方法,但又抽象方法的类必须是抽象类
-
子类必须覆盖重写抽象父类中所有的抽象方法
-
必须用一个子类来继承抽象方法
public class Cat extends Animal { public Cat(String name) { super(name); } @Override public void eat() { System.out.println("猫吃鱼"); } } // 使用 Animal cat1 = new Cat("Tom"); // 多态的写法 左边父类(抽象类)右边实现类 Cat cat2 = new Cat("JiMu"); cat1.eat(); // "猫吃鱼" cat2.eat(); // "猫吃鱼"
-
-
发红包案例
-
用户类
// 用户类 public class User { private String name; private int money; // 余额 public User() { } public User(String name, int money) { this.name = name; this.money = money; } public void show() { System.out.println("我是" + name + ", 我有多少钱:" + money); } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getMoney() { return money; } public void setMoney(int money) { this.money = money; } }
-
发红包的群主类
// 群主 public class Manager extends User { public Manager() { } public Manager(String name, int money) { super(name, money); } public ArrayList<Integer> send(int totalMoney, int count) { // 用集合来存储若干个红包的余额 ArrayList<Integer> redList = new ArrayList<>(); // 看群主有多少余额 int leftMoney = super.getMoney(); if(totalMoney > leftMoney) { // 余额不足 System.out.println("余额不足!"); return redList; } // 扣钱 super.setMoney(leftMoney - totalMoney); // 发红包,平均每个红包多少 int avg = totalMoney/count; int mod = totalMoney % count; // 余数 // 剩下的零头,包在最后一个红包当中 // 下面准备包红包 for (int i = 0; i < count - 1; i++) { redList.add(avg); } // 最后一个红包 int last = avg + mod; redList.add(last); return redList; } }
-
抢红包的成员类
// 普通成员 public class Member extends User { public Member() { } public Member(String name, int money) { super(name, money); } public void receive(ArrayList<Integer> list) { // 从多个红包中随便抽取一个给自己 // 随机获取一个集合当中的编号 int index = new Random().nextInt(list.size()); // 根据索引从集合当中删除,并且得到被删除的红包给自己 int delta = list.remove(index); // 当前成员有多少钱 int money = super.getMoney(); super.setMoney(money + delta); } }
-
测试类
public static void main(String[] args) { Manager manager = new Manager("群主", 100); // 群主有100元 Member one = new Member("A", 0); Member two = new Member("B", 0); Member three = new Member("C", 0); manager.show(); // 100 one.show(); // 0 two.show(); // 0 three.show(); // 0 System.out.println("=========================="); ArrayList<Integer> redList = manager.send(59, 3); one.receive(redList); two.receive(redList); three.receive(redList); manager.show(); one.show(); two.show(); three.show(); }
-
1.7. 接口
-
接口中可以包含的内容有
- 常量 (Java 7)
- 抽象方法 (Java 7)
- 默认方法 (Java 8)
- 静态方法 (Java 8)
- 私有方法 (Java 9)
-
接口的 抽象方法定义
- 接口当中抽象方法的注意事项
- 修饰符必须是两个固定的关键字:public abstract
- 这两个关键字可以选择性省略
- 方法的三要素可以随意定义
public interface MyInterface { public abstract void methodA(); public void methodB(); abstract void methodC(); void methodD(); }
- 接口当中抽象方法的注意事项
-
接口的使用步骤
- 接口不能直接使用,必须有一个实现类实现该接口
- 该接口的实现类必须覆盖重写接口中所有的抽象方法
- 创建实现类的对象进行使用
// 注意事项:如果实现类并没有覆盖重写接口中所有的抽象方法,那么这个实现类必须是抽象类 // 实现类 public class MyInterfaceImpl implements MyInterface { @Override public void methodA() { System.out.println("methodA"); } }
-
接口中的默认方法定义
-
从 Java 8 开始,接口里允许定义默认方法
-
备注:接口当中的默认方法,可以解决接口升级的问题
public default 返回值类型 方法名称(参数列表) { 方法体 }
-
-
接口的默认方法使用
public default void methodDefault(){ System.out.println("这是一个新添加的默认方法"); } // 创建实现类对象 MyInterfaceImpl myInterface = new MyInterfaceImpl(); myInterface.methodA(); // 调用默认方法,如果实现类当中没有,会向上找接口 myInterface.methodDefault();
-
接口的静态方法定义
- 从 Java 8 开始,接口里允许定义静态方法
- 提示:就是将 abstract 或者 default 换成 static 即可,带上方法体
public static 返回值类型 方法名称(参数列表) { 方法体 }
-
接口的静态方法使用
- 注意:不能通过接口实现类的对象来调用接口当中的静态方法
public static void methodStatic() { System.out.println("这是接口的静态方法"); } myInterface.methodStatic // 通过实现类的对象来.接口中的静态方法 => 错误写法 // 调用格式:接口名称.静态方法名(参数) MyInterface.methodStatic(); // 这是接口的静态方法
-
接口的私有方法定义
- 从 Java 9 开始,接口当中允许定义私有方法
- 普通私有方法,解决多个默认方法之间重复代码问题
- 静态私有方法,解决多个静态方法之间重复代码问题
普通私有方法格式:private 返回值类型 方法名称(参数列表) { 方法体 } 静态私有方法格式:private static 返回值类型 方法名称(参数列表) { 方法体 }
- 从 Java 9 开始,接口当中允许定义私有方法
-
接口的私有方法使用
private void defaultCommon() { System.out.println("AAA"); } private static void StaticCommon() { System.out.println("BBB"); }
-
接口的常量定义和使用
- 一旦使用了 final 关键字进行修饰就不能改变
- 注意事项
- 接口当中的常量,可以省略 public static final ,不写也是这样
- 接口中的常量,必须赋值,不能不赋值
- 接口中常量的名称,使用完全大写的字母,用下划线进行分隔(推荐命名)
接口定义常量的格式 public static final 数据类型 常量名称 = 数据值; // 使用格式:接口名称.常量名称 int num = MyInterface.NUM_OF_MY_CLASS;
-
接口的内容小结
- 成员变量其实是常量,格式:[public] [static] [final] 数据类型 常量名称 = 数据值;
- 常量一旦赋值就不能改变,必须赋值,常量名称完全大写,用下划线分隔
- 接口中最重要的是抽象方法:格式 [public] [abstract] 返回值类型 方法名称(参数列表);
- 注:实现类必须要覆盖重写接口所有的抽象方法,除非是抽象类
- 从 Java 8 开始,接口里允许定义默认方法,格式:[public] default 返回值类型 方法名称(参数列表) { 方法体 }
- 从 Java 8 开始,接口里允许定义静态方法,格式:[public] static 返回值类型 方法名称(参数列表) { 方法体 }
- 注:应该通过接口名称进行调用,不能通过实现类对象来调用
- 从 Java 9 开始,接口中允许定义私有方法
- 普通私有方法格式:private 返回值类型 方法名称(参数列表) { 方法体 }
- 静态私有方法格式:private 返回值类型 方法名称(参数列表) { 方法体 }
- 注:被 private 修饰的方法只有接口自己才能使用,不能被实现类或别人使用
- 接口不能有静态代码块和构造方法
static {} // 在接口中使用静态代码块是 => 错误写法 public MyInterface() {} // 在接口中使用构造方法是 => 错误写法
-
实现类可以实现多个接口,继承类只能继承一个类
-
一个实现类实现了多个接口且接口当中有重复的默认方法,实现类必须要覆盖重写该方法
-
一个接口可以继承多个接口,若继承的多个父接口当中默认方法重复,那么子接口必须进行默认方法的覆盖重写,并且带着 default 关键字
-
笔记本 USB 接口案例分析 使用接口和多态
- USB 接口,包含打开设备功能、关闭设备功能
- 笔记本类,包含开机功能、关机功能、使用 USB 设备功能
- 鼠标类,要实现 USB 接口,并具备点击的方法
- 键盘类,要实现USB 接口,具备敲击的方法
-
案例实现
- USB 接口
public interface USB { public abstract void open(); // 打开设备 public abstract void close(); // 关闭设备 }
- Laptop 笔记本类
public class Laptop { public void powerOn() { System.out.println("笔记本电脑开机"); } public void powerOff() { System.out.println("笔记本电脑关机"); } // 使用 USB 设备的方法,使用接口作为方法的参数 public void useDevice(USB usb) { usb.open(); if(usb instanceof Mouse) { Mouse mouse = (Mouse)usb; mouse.click(); } else if(usb instanceof KeyBoard) { KeyBoard keyBoard = (KeyBoard) usb; keyBoard.type(); } usb.close(); } }
- Mouse 鼠标实现类
public class Mouse implements USB { @Override public void open() { System.out.println("打开鼠标"); } @Override public void close() { System.out.println("关闭鼠标"); } public void click() { System.out.println("鼠标点击"); } }
- KeyBoard 键盘实现类
public class KeyBoard implements USB { @Override public void open() { System.out.println("打开键盘"); } @Override public void close() { System.out.println("关闭键盘"); } public void type() { System.out.println("键盘按下"); } }
- 测试
// 创建笔记本对象 Laptop laptop = new Laptop(); laptop.powerOn(); // 向上转型 USB usb = new Mouse(); laptop.useDevice(usb); laptop.useDevice(new KeyBoard()); laptop.powerOff();
1.7.1. 接口作为成员变量类型
-
英雄类 Hero
public class Hero { private String name; // 英雄的名称 private Skill skill; // 英雄的技能 public Hero() { } public Hero(String name, Skill skill) { this.name = name; this.skill = skill; } public void attack() { System.out.println("我叫" + name + ", 开始释放技能:"); skill.use(); System.out.println("释放技能完成"); } public String getName() { return name; } public void setName(String name) { this.name = name; } public Skill getSkill() { return skill; } public void setSkill(Skill skill) { this.skill = skill; } }
-
技能接口 Skill
public interface Skill { void use(); // 释放技能的抽象方法 }
-
测试类
// 创建英雄角色 Hero hero = new Hero(); // 设置英雄名称 hero.setName("艾希"); // hero.setSkill(new SkillImpl()); 传实现类的方法 hero.setSkill(() -> System.out.println("哔哔哔")); // 传匿名对象的方法 hero.attack(); // 我叫艾希, 开始释放技能:哔哔哔 释放技能完成
1.8. 内部类
-
成员内部类的定义
- 注意:内用外,随意访问;外用内,需要内部类对象
public class Body { // 外部类 public class Heart { // 成员内部类 // 内部类的方法 public void beat() { System.out.println("蹦蹦蹦!"); System.out.println("内部类方法" + name); } } // 外部类的方法 public void methodBody() { System.out.println("外部类的方法"); new Heart().beat(); } // 外部类的成员变量 private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } }
-
成员内部类的使用
// 间接方式:在外部类的方法当中,使用内部类;然后在 main 只是调用外部类的方法 Body body = new Body(); // 通过外部类的对象,调用外部类的方法,里面间接在使用内部类 Heart body.methodBody(); // 直接方式 【外部类名称.内部类名称 对象名 = new 外部类名称().new 内部类名称();】 Body.Heart heart = new Body().new Heart(); heart.beat();
-
内部类的同名变量访问
public class Outer { int num = 10; public class Inner { int num = 20; public void methodInner() { int num = 30; System.out.println(num); // 30 System.out.println(this.num); // 20 System.out.println(Outer.this.num); // 10 } } }
-
局部内部类
- 如果一个类定义在一个方法内部的,那么这就是一个局部内部类
- 只能当前所属的方法才能使用它,出了该方法就不能用了
- 注:定义一个类的时候,权限修饰符规则
- 外部类:public / (default)
- 成员内部类:public / protected / (default) / private
- 局部内部类:什么都不能写
- 局部内部类,如果希望访问所在的方法的局部变量,那么这个局部变量必须是有效的【final】
- 从 Java 8 + 开始,只要局部变量事实不变,那么 final 关键字可以省略
- 原因:【生命周期】方法结束后,局部变量要立即消失,在 new 出来的对象会在堆中持续存在,直到垃圾回收消失
- 从 Java 8 + 开始,只要局部变量事实不变,那么 final 关键字可以省略
public class Outer { public void methodOuter() { class Inner { int num = 10; public void methodInner() { System.out.println(num); // 10 } } Inner inner = new Inner(); inner.methodInner(); } }
- 如果一个类定义在一个方法内部的,那么这就是一个局部内部类
-
匿名内部类
- 如果接口的实现类(或者是父类的子类)只需要使用唯一的一次这种情况下就可以省略该类的定义,而改为使用【匿名内部类】
- 匿名内部类的注意事项
- 匿名内部类,在【创建对象】的时候,只能使用唯一一次,如果希望多次创建对象,而且类的内容一样的话,那么就必须使用单独定义的实现类
- 匿名对象,在【调用方法】的时候,只能调用唯一一次,如果希望同一个对象调用多次方法,那么必须给对象起个名字
- 匿名内部类省略【实现类 / 子类】但是匿名对象省略了【对象名称】
MInterface mInterface = new MInterface() { @Override public void method() { // 匿名内部类实现的方法 System.out.println("hello"); } }; mInterface.method(); // hello // new MInterface() { @Override public void method() { // 匿名内部类实现的方法 System.out.println("hello"); } }.method();
1.9. 发红包案例
1.9.1. 案例分析
-
红包分发策略
- 普通红包(平均):totalMoney / totalCount,余数放在最后一个红包当中
- 手气红包(随机):最少一分钱,最多不超过平均数的 2 倍,余额越发越少
-
需要做的:
- 设置一下程序的标题,通过构造方法的字符串参数
- 设置群主名称
- 设置分发策略,平均,还是随机
1.9.2. 代码实现
-
红包打开模式接口 OpenMode
public interface OpenMode { /** * 将totalMoney分成count份,保存到ArrayList<Integer>中,返回即可。 * * @param totalMoney 总金额为方便计算,已经转换为整数,单位为分。 * @param totalCount 红包个数 * @return ArrayList<Integer> 元素为各个红包的金额值,所有元素的值累和等于总金额。 */ ArrayList<Integer> divide(int totalMoney, int totalCount); }
-
继承 RedPacketFrame 类,实现设置红包标题类 MyRed(生成红包界面类)
public class MyRed extends RedPacketFrame { /** * 构造方法:生成红包界面。 * * @param title 界面的标题 */ public MyRed(String title) { super(title); } }
-
实现接口 OpenMode 的普通红包类 NormalMode
public class NormalMode implements OpenMode { @Override public ArrayList<Integer> divide(final int totalMoney, final int totalCount) { ArrayList<Integer> list = new ArrayList<>(); int avg = totalMoney / totalCount; // 平均值 int mod = totalMoney % totalCount; // totalCount -1 代表 最后一个先留着 for(int i = 0; i < totalCount -1; i++) { list.add(avg); } // 有零头需要放在最后一个 list.add(avg + mod); return list; } }
-
实现接口 OpenMode 的随机红包类 RandomMode
public class RandomMode implements OpenMode { @Override public ArrayList<Integer> divide(int totalMoney, int totalCount) { ArrayList<Integer> list = new ArrayList<>(); // 随机分配, 有可能多, 有可能少 // 最少一分钱, 最多不超过剩下金额平均数的 2 倍 如发 10 块 发 3 个 下面分析 /* 第一次发红包, 随机范围是 0.01~6.66元 第一次发完之后, 剩下的至少是 3.34 元 此时还需再发 2 个红包 然后再发的范围是 0.01~3.34 元(取不到右边, 至少剩下0.01) 【范围的公式】:1 +random.nextInt(LeftMoney / LeftCount * 2); */ Random r = new Random(); // 首先随机创建一个随机生成器 // 定义两个变量, 分别代表剩下多少钱, 剩下多少份 int leftMoney = totalMoney; int leftCount = totalCount; // 随机发前 n-1 个, 最后一个不需要随机 for(int i = 0; i < totalCount - 1; i++) { // 按照公式生成随机金额 int money = r.nextInt(leftMoney / leftCount * 2) + 1; // 将一个随机红包放入集合 list.add(money); // 剩下的金额 leftMoney -= money; // 剩下应该再发的红包个数递减 leftCount--; } // 最后一个红包不需要随机, 直接放 list.add(leftMoney); return list; } }
-
测试
public class Bootstrap { public static void main(String[] args) { MyRed red = new MyRed("传智博客双元课程"); red.setOwnerName("王思聪"); /* 普通红包 OpenMode normal = new NormalMode(); red.setOpenWay(normal); */ // 随机红包 OpenMode random = new RandomMode(); red.setOpenWay(random); } }
*
二. Java 进阶篇
2.1. 基础篇
2.1.1. 时间类
-
毫秒值(1000毫秒 == 1秒)
- java.util.Date:表示时间和日期进行计算
// 获取当前系统时间从 1970 年 1 余额 1 日 00:00:00 经历了多少毫秒 System.out.println(System.currentTimeMillis()); // 1610526602507
-
Date 类的构造方法和成员方法
// 构造方法 Date d = new Date(); // 无参的构造方法 System.out.println(d); // Wed Jan 13 16:33:16 CST 2021 Date d1 = new Date(0); // 有参的构造方法 System.out.println(d1); // Thu Jan 01 08:00:00 CST 1970 Date d2 = new Date(1610526602501L); System.out.println(d2); // Wed Jan 13 16:30:02 CST 2021 // 成员方法 // 把日期转换为毫秒 相当于 System.currentTimeMillis() System.out.println(d.getTime()); // 1610527026494
-
格式化日期 DateFormat(抽象类) 和 SimpleDateFormat 类
- 作用:格式化(日期 —> 文本)、解析(文本 —> 日期)
- 成员方法
- String format(Date date):按照指定的模式,将 Date 日期格式化为符合模式的字符串
- Date parse(String source):把符合模式的字符串解析为 Date 日期
- 构造方法
- SimpleDateFormat(String pattern):指定将日期时间格式化字符串的模式
Date d = new Date(); DateFormat dFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); System.out.println(dFormat.format(d)); // 2021-01-13 16:50:40 // 将符合格式的日期时间字符串解析成 Date 日期 dateFormat.parse("2021-01-13 16:50:40"); // Wed Jan 13 16:50:40 CST 2021
-
练习 :计算从出生到现在有多少天
// 当前日期 Date now = new Date(); // 输入的格式 System.out.println("请输入您的生日 格式:YYYY-MM-dd"); DateFormat dFormat = new SimpleDateFormat("yyyy-MM-dd"); String birthdayStr = new Scanner(System.in).nextLine(); // 将输入的日期字符串解析为日期 Date birthday = dFormat.parse(birthdayStr); // 计算天数 long days = (now.getTime() - birthday.getTime()) / 1000 / 3600 / 24; System.out.println(days);
-
日历类 Calender (抽象类)
- java.util.Calender:提供了很多操作日历字段的方法(YEAR、DAY_OF、HOUR)
- Calender 还有一个静态方法:getInstance(),该方法返回了 Calender 类的子类对象
// 拿到 Calender 类的子类对象 Calendar calendar = Calendar.getInstance();
-
常用方法
- public int get(int field):返回给定日历字段的值
- public void set(int field, int value):将给定的日历字段设置为给定值
- 注:设置月的范围是 0-11
- public abstract void add (int field, int amount):根据日历规则,为给定的日历字段添加或减去指定的时间值
- public Date getTime():返回一个表示此 Calender 时间值(从历元到现在的毫秒偏移量)的 Date 对象
// 获取日历 public static void getTimeStr(Calendar calendar) { // 获取年 int year = calendar.get(Calendar.YEAR); // 2021 // 获取月, 范围 0-11 int month = calendar.get(Calendar.MONTH); // 1 // 获取多少号 int date = calendar.get(Calendar.DATE); // 13 int dateOfMonth = calendar.get(Calendar.DAY_OF_MONTH); // 13 System.out.println(year + "-" + month + "-" + date + " " + dateOfMonth); } // 拿到 Calender 类的子类对象 Calendar calendar = Calendar.getInstance(); calendar.set(Calendar.YEAR, 9999); // 9999-1-13 13 // 设置月的范围是 0-11 calendar.set(Calendar.MONTH, 9); // 9999-9-13 13 calendar.set(Calendar.DATE, 9); // 9999-9-9 9 // 同时设置年月日 calendar.set(8888, 8, 8); // 8888-8-8 8 // 把指定的字段增加或减少 calendar.add(Calendar.YEAR, 2); // 8890-8-8 8 calendar.add(Calendar.YEAR, -3); // 8887-8-8 8 System.out.println(calendar.getTime()); // Mon Sep 08 17:40:25 CST 8887 getTimeStr(calendar);
2.1.2. System 类常用方法
-
java.lang.System:提供了大量的静态方法,可以获取与系统相关的信息或系统级操作
- public static long currentTimeMills():返回以毫秒为单位的当前时间
- public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
- 将数组中指定的数据拷贝到另一个数组中
- src:源数组,srcPos:源数组中的起始位置,dest:目标数组
- destPos:目标数组中的起始位置,length:要复制的数组元素的数量
// 获取系统时间 long currTime = System.currentTimeMillis(); // 1610531920738 // 拷贝数组 int[] src = {1, 3, 44, 75, 12, 36}; int[] dest = {58, 86 ,89, 40, 14, 5, 81}; System.out.println("复制前:" + Arrays.toString(dest)); // [58, 86, 89, 40, 14, 5, 81] System.arraycopy(src, 0, dest, 0, 5); System.out.println("复制后:" + Arrays.toString(dest)); // [1, 3, 44, 75, 12, 5, 81]
2.1.3. StringBuilder 类
-
String 类:字符串是常量,它们的值在创建之后就不能改变
- 字符串的底层是一个被 final 修饰的数组,不能改变,因为它是一个常量
- 进行字符串的相加,内存中就会有多个字符串,占用空间多,效率低下
-
StringBuilder 类:字符串缓冲区,可以提高字符串的操作效率(看成一个长度可以变化的字符串)
- 底层是一个数组,但是没有被 final 修饰,可以改变长度
-
StringBuilder 的构造方法和 append 方法(可以使用链式操作)
StringBuilder sb1 = new StringBuilder(); System.out.println(sb1); // 空 // append 方法返回的是 this, 调用方法的对象 sb2 所有使用 append 方法是无需接收返回值 StringBuilder sb2 = sb1.append("java"); // 使用 append 方法往 sb 中添加数据 sb1.append(" to me!"); System.out.println(sb1); // java to me! System.out.println(sb1 == sb2); // true 两个对象是同一个对象
-
StringBuilder 的 toString()
String str1 = "java"; System.out.println(str1); // java // 将 String 转换为 StringBuilder StringBuilder sb = new StringBuilder(str1); sb.append(" to me !"); System.out.println(sb); // java to me ! // 将 StringBuilder 对象转换为 String String str2 = sb.toString(); System.out.println(str2); // java to me !
2.1.4. 包装类
-
Java 提供了两个类型,基本类型和引用类型,使用基本类型在于效率,但是不能像对象那样做更多的功能,我们可以将基本类型转换为对应的包装类就可以使用更多的功能
-
装箱与拆箱
- 装箱:把基本类型的数据包装到包装类中(基本类型 ----> 包装类)
- Integer(int value):构造一个新分配的 Integer 对象,它表示指定的 int 值
- Integer(String s):构造一个新分配的 Integer 对象,它表示 String 参数所指示的 int 值
- 拆箱:在包装类中取出基本类型的数据(包装类 ----> 基本类型的数据)
- int intValue():以 int 类型返回该 Integer 的值
Integer int1 = new Integer(1); // // 构造方法已过时 Integer in2 = new Integer("2"); // 构造方法已过时 // 装箱 Integer in3 = Integer.valueOf(10); Integer in4 = Integer.valueOf("1"); System.out.println(in3 + " " + in4); // 拆箱 int i = in3.intValue(); System.out.println(i);
- 装箱:把基本类型的数据包装到包装类中(基本类型 ----> 包装类)
-
自动装箱与自动拆箱
- 基本类型的数据和包装类之间可以自动的相互转换(jdk1.5 之后的特性)
Integer in = 1; // 会自动拆箱再装箱 in = in + 2; // 3 ArrayList<Integer> list = new ArrayList<>(); list.add(1); // 自动装箱 list.add(Integer.valueOf(1)) int i = list.get(0); // 自动拆箱 list.get(0).intValue();
-
基本类型与字符串类型之间的相互转换
- 基本类型 => 字符串
- 最简单的方式:“+” (常用)
- 使用包装类的静态方法:static String toString(int i):返回一个表示指定的 String 对象
- 使用 String 中的静态方法:static String valueOf(int i):返回 int 参数的字符串表现形式
- 字符串 => 基本类型
- 使用包装类的静态方法:parseXX(“字符串”)
- Integer类:static int Integer.parseInt(String s)
- Double类:static double Double.parseDouble(String s)
- 使用包装类的静态方法:parseXX(“字符串”)
String str1 = 100 + "2"; String str2 = Integer.toString(200); String str3 = String.valueOf(10); System.out.println(str1 + str2 + str3); // 10020010 int i = Integer.parseInt(str1); System.out.println(i); // 1002
- 基本类型 => 字符串
2.1.5 自定义异常类
-
模拟注册操作,如果用户已存在,则抛出异常并提示,亲,该用户名已存在
- 异常类
// RuntimeException 运行期异常, 出错后会交给 JVM 处理 // Exception 自己处理 public class RegisterException extends /*Exception*/ RuntimeException { public RegisterException() { } public RegisterException(String message) { super(message); } }
- 测试
static String[] usernames = {"Java", "JavaScript", "Vue"}; public static void main(String[] args) { System.out.println("请输入用户名"); String username = new Scanner(System.in).next(); checkUsername(username); } // 对用户输入进行校验 public static void checkUsername(String username) { for (String name : usernames) { if(name.equals(username)) { /* 自己处理 try { throw new RegisterException("亲, 该用户名已存在"); } catch (RegisterException e) { e.printStackTrace(); return; // 结束方法 } */ throw new RegisterException("亲, 该用户名已存在"); // JVM 处理会中断程序 } } System.out.println("恭喜注册成功"); }
2.2. 集合
2.2.1 Collection 接口
-
学习集合的目标:
- 会使用集合存储数据
- 会遍历集合,把数据取出来
- 掌握每种集合的特性
-
Collection 接口:
- 定义的是所有单列集合中共性的方法
- 所有的单列集合都可以使用共性的方法(没有带索引的方法)
-
Collection 集合常用方法(实现它的接口都可以使用)
- boolean add(E e):向集合中添加元素
- boolean remove(E e):删除集合中的某个元素
- void clear():清空集合所有的元素
- boolean contains(E e):判断集合中是否包含某个元素
- boolean isEmpty():判断集合是否为空
- int size():获取集合的长度
- Object[] toArray():将集合转成一个数组
Collection<String> coll = new ArrayList<>(); coll.add("java"); coll.add(" to me"); // 判断 java 元素是否在集合 coll 中 System.out.println(coll.contains("java")); // true // 获取集合的长度 System.out.println(coll.size()); // 2 // 将集合转成一个数组 Object[] arr = coll.toArray(); for (int i = 0; i < arr.length; i++) { System.out.print(arr[i] + " "); } System.out.println(); // java to me System.out.println(coll); // [java, to me] // 将 java 元素删除 coll.remove("java"); System.out.println(coll); // [ to me] // 清空集合中所有元素 coll.clear(); System.out.println(coll); // [] // 判断集合是否为空 System.out.println(coll.isEmpty()); // true
-
List 接口:
- 有序的集合(存储和取出元素顺序相同)
- 有索引,可以使用普通的 for 循环遍历
- 允许有存储重复的元素;
- 实现该接口的集合:
- Vector 集合(了解)
- ArrayList 集合(重点):底层是数组实现的,查询快、增删慢
- LinkedList 集合(次之):底层是链表实现的,查询慢、增删快
-
Set 接口:
-
不允许存储重复元素;
-
没有索引,无序(不能使用普通 for 循环遍历)
-
实现该接口的集合有:
-
TreeSet 集合(了解):底层是二叉树实现,一般用于排序
-
HashSet 集合(重点):底层是哈希表+(红黑树)实现的、不可以存储重复元素,存取无序
-
LinkedHashSet 集合(次之):底层是哈希表+链表实现,无索引,不可存储重复元素,可以保证存取顺序
-
-
-
斗地主案例
- 案例分析
- 准备牌,存在一个集合中,特殊牌:大王、小王,其他52张牌 组装,
- 洗牌:collections 的方法 static void suffle(List<?> list)
- 发牌,一人17张牌,最后三张作为底牌,一张一张是发
- 看牌:遍历三个玩家的牌和底牌
- 案例实现
// 用牌存储组装好的牌 ArrayList<String> poker = new ArrayList<>(); // 存储牌的花色和牌的序号 String[] colors = { "♠", "♥", "♣", "♦" }; String[] numbers = { "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A" }; poker.add("大王"); poker.add("小王"); for(String number : numbers) { for(String color : colors) { // 牌一张一张的装进 poker 中 poker.add(color + number); } } // 洗牌 Collections.shuffle(poker); // 存储3个玩家的牌和底牌 ArrayList<String> play1 = new ArrayList<>(); ArrayList<String> play2 = new ArrayList<>(); ArrayList<String> play3 = new ArrayList<>(); ArrayList<String> diPai = new ArrayList<>(); for(int i = 0; i < poker.size(); i++) { String p = poker.get(i); if(i < 51) { // 开始轮流发牌 if(i % 3 == 0) { play1.add(p); } else if(i % 3 == 1) { play2.add(p); } else if(i % 3 == 2) { play3.add(p); } } else { // 底牌 diPai.add(p); } } // [♥8, ♣9, ♥2, ♣3, ♦Q, ♠7, ♠3, ♣8, ♦9, ♠10, ♥A, ♥4, ♥K, ♥7, ♦6, ♦J, ♦10] System.out.println(play1); // [♥5, ♣10, ♣K, 大王, ♠4, ♦7, 小王, ♣2, ♣J, ♦5, ♣5, ♥3, ♥Q, ♠Q, ♦4, ♣6, ♠6] System.out.println(play2); // [♠9, ♣Q, ♦K, ♥6, ♣4, ♠2, ♠8, ♦8, ♠A, ♠5, ♦2, ♣7, ♦3, ♣A, ♥10, ♠J, ♦A] System.out.println(play3); // [♥J, ♠K, ♥9] System.out.println(diPai);
- 案例分析
2.2.2. List 接口
-
有序的(有索引),可以存储重复的元素
-
List 接口中的特有方法
- public void add(int index, E elment):将指定的元素添加到集合的指定位置上
- public E get(int index):返回集合中指定位置的元素
- public E remove(int index):移除集合中指定位置的元素,返回被移除的元素
- public E set(int index, element):用指定元素替换集合中指定位置的元素,返回更新前的元素
List<String> list = new ArrayList<>(); list.add("Java"); list.add("JavaScript"); list.add(1, "to"); System.out.println(list); // [Java, to, JavaScript] System.out.println(list.remove(1)); // to System.out.println(list.set(1, "To")); // JavaScript 返回更新前的元素 System.out.println(list.get(1)); // To System.out.println(list); // [Java, To]
-
ArrayList 实现类的底层是用数组来实现的,所以查询快,增删慢
-
LinkedList 实现类(用链表实现的,查询慢,增删快)
-
常用的方法(里面有大量操作首尾的方法)注:不能使用多态
- public void addFirst(E e):将指定元素插入到集合的开头
- public void adLast(E e):将指定元素插入到集合的末尾(等效于 add(E e) 方法
- public E getFirst():返回集合中的第一个元素
- public E getLast():返回集合中的最后一个元素
- public E removeFirst():移除并返回集合中的第一个元素
- public E removeLast():移除并返回集合中的最后一个元素
- public void push(E e):等效于 addFirst() 方法
- public E pop():移除第一个元素 等同于 removeFirst 方法
LinkedList<String> linked = new LinkedList<>(); linked.addFirst("Java"); // 将 Java 添加到集合的第一个元素中 linked.addLast("JavaScript"); // 将 JavaScript 添加到集合的最后一个元素中 linked.add("bright"); // 等效于 addLast 方法 linked.addFirst("Like"); System.out.println(linked); // [Like, Java, JavaScript, bright] linked.push("I"); // 等同于 addFirst 方法 System.out.println(linked); // [I, Like, Java, JavaScript, bright] if(!linked.isEmpty()) { // 判断集合是否为空 System.out.println(linked.getFirst()); // I System.out.println(linked.getLast()); // bright } // linked.clear(); 清空集合 获取时没有元素将报错 System.out.println(linked.removeFirst()); // I 移除并返回第一个元素 System.out.println(linked.removeLast()); // bright 移除并返回最后一个元素 System.out.println(linked.pop()); // 等同于 removeFirst 方法 Like 移除并返回第一个元素 System.out.println(linked); // [Java, JavaScript]
-
Vector 集合:底层也是由数组实现的(被 ArrayList 取代了)单线程的,所以较慢
2.2.3. Set 接口
-
Set 接口的特点
- 不允许存储重复的元素
- 没有索引(遍历只能使用增强 for 循环或者迭代器
-
HashSet 集合实现类:底层是哈希表结构(查询的速度非常快)
Set<Integer> set = new HashSet<>(); set.add(10); set.add(20); set.add(30); set.add(30); // 不允许重复的元素 Iterator<Integer> iter = set.iterator(); while(iter.hasNext()) { System.out.print(iter.next() + " "); // 20 10 30 } System.out.println(); for(Integer i : set) { System.out.print(i + " "); // 20 10 30 }
-
HashSet 存储自定义类型元素
- 注:// 使用 HashSet 存储自定义类型, 必须重写 equals 和 hashCode 方法
// 自定义存储 Person 类 HashSet<Person> set = new HashSet<>(); Person p1 = new Person("Java", 56); Person p2 = new Person("Java", 56); Person p3 = new Person("Java", 59); set.add(p1); set.add(p2); set.add(p3); // [Person{name='Java', age=56}, Person{name='Java', age=59}] System.out.println(set); public class Person { private String name; private int age; public Person() { } public Person(String name, int age) { this.name = name; this.age = age; } // 使用 HashSet 存储该对象, 必须重写 equals 和 hashCode 方法 @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Person person = (Person) o; return age == person.age && Objects.equals(name, person.name); } @Override public int hashCode() { return Objects.hash(name, age); } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } }
-
LinkedHashSet 集合实现类:底层是哈希表(数组+链表/红黑树)
- 多了一条链表(记录元素的存储顺序),保证元素有序
// 元素不能重复, 有序 LinkedHashSet<String> set = new LinkedHashSet(); set.add("Java"); set.add("JavaScript"); set.add("Php"); set.add("Java"); // 不能重复 System.out.println(set); // [Java, JavaScript, Php]
2.2.4. Map 接口
-
Map 集合的特点
- Map 集合是一个双列集合,一个元素包含两个值(key,value)
- Map 集合中的元素,key 和 value 的数据类型可以相同,也可不同
- Map 集合中的元素,key 不允许重复,value 可以重复
- Map 集合中,key 和 value是一一对应的
-
HashMap 实现类:底层是哈希表:查询的速度特别的快
- 无序的集合
-
LinkedHashMap 集合 extends HashMap<k, v>
- LinkedHashMap 集合底层是哈希表+链表,有序的集合
-
Map 接口中的常用方法
- public V put (K key, V value):把指定的键与指定的值添加到 HashMap 集合中
- 如果 key 存在,则返回替换前的值,否则返回 null
- public V remove(Object key):把指定的值键所对应的键值元素在Map集合中删除,返回被删除元素的值
- key 存在,v 返回被删除的值
- key 不存在,v 返回 null
- public V get(Object key):根据指定的键,在 Map 集合中获取对应的值
- boolean containsKey(Obejct key):判断集合中是否包含指定的值
- public Set keySet():获取 Map 集合中所有的键,存储到 Set 集合中
- public Set <Map.Entry<K, V>> entrySet():获取到 Map 集合中
Map<String, String> map = new HashMap<>(); // 返回值 v1: key 存在, 替换并返回被替换的值, 所以返回 null String v1 = map.put("language", "Java"); // key 存在, v 返回被删除的值; 不存在, v 返回 null String language = map.remove("language"); // Java map.put("language", "Java"); map.put("name", "Tom"); map.put("class", "软信1802"); System.out.println(map); // map.get(key) 获取对应 key 的值 System.out.println(map.get("name")); // true // 判断 map 集合中是否包含键 name System.out.println(map.containsKey("name")); // true
- public V put (K key, V value):把指定的键与指定的值添加到 HashMap 集合中
-
Map 集合遍历的方式
- 遍历键找值方式
- 所有的 key 取出来放到 Set 集合
Map<String, String> map = new HashMap<>(); map.put("age", "19"); map.put("language", "Java"); map.put("name", "Tom"); map.put("class", "软信1802"); // 获取 map 集合的所有 key 并放入 Set 集合中 Set<String> keys = map.keySet(); /* 使用的迭代器来遍历 Set 集合 Iterator<String> iter = keys.iterator(); while(iter.hasNext()) { String key = iter.next(); System.out.println(key + "=>" + map.get(key)); } */ // 使用增强 for 来遍历 Set 集合 for(String key : keys) { System.out.println(key + "=>" + map.get(key)); }
- Entry 键值对对象
- Map.Entry<K, V> 在 Map 接口中有一个内部接口 Entry
- 作用:当 Map 集合一创建,就会在 Map 集合中创建 Entry 对象,用来记录(键值对对象,键与值的对应关系)
- Map.Entry<K, V> 在 Map 接口中有一个内部接口 Entry
Map<String, String> map = new HashMap<>(); map.put("age", "19"); map.put("language", "Java"); map.put("name", "Tom"); map.put("class", "软信1802"); // 使用 map.entrySet() 取出 map 集合所有的键值对对象 Set<Map.Entry<String, String>> set = map.entrySet(); // 通过迭代器遍历 Iterator<Map.Entry<String, String>> iter = set.iterator(); while(iter.hasNext()) { Map.Entry<String, String> entry = iter.next(); String key = entry.getKey(); String value = entry.getValue(); System.out.println(key + "=>" + value); } /* 通过 增加 for 循环遍历 for(Map.Entry<String, String> entry : set) { String key = entry.getKey(); String value = entry.getValue(); System.out.println(key + "=>" + value); } */
- 遍历键找值方式
-
HashMap 存储自定义类型键值
- map 集合保证 key 是唯一的,作为 key 的元素,String 类型是重写了那两个方法了的
- 如果键是自定义的类型,比如类作为键,则必须重写 HashCode 和 equals 方法
/* HashMap<String, Person> map = new HashMap<>(); map.put("北京", new Person("Java", 59)); map.put("上海", new Person("JavaScript", 40)); map.put("深圳", new Person("Node", 36)); map.put("杭州", new Person("Vue", 30)); Set<String> set = map.keySet(); for(String key : set) { System.out.println(key + "=>" + map.get(key)); } */ // Person 作为键 HashMap<Person, String> map = new HashMap<>(); map.put(new Person("Java", 59), "北京"); map.put(new Person("Java", 59), "上海"); // 值会被覆盖 map.put(new Person("Vue", 36), "深圳"); map.put(new Person("Node", 30), "杭州"); Set<Person> set = map.keySet(); for(Person key : set) { System.out.println(key + "=>" + map.get(key)); } /* 输出 Person{name='Node', age=30}=>杭州 Person{name='Vue', age=36}=>深圳 Person{name='Java', age=59}=>上海 */
- map 集合保证 key 是唯一的,作为 key 的元素,String 类型是重写了那两个方法了的
-
LinkedHashMap 集合 继承了 HashMap 集合,是一个有序的集合
LinkedHashMap<String, Integer> linkedMap = new LinkedHashMap<>(); linkedMap.put("Java", 59); linkedMap.put("JavaScript", 50); linkedMap.put("Vue", 36); linkedMap.put("Node", 19); System.out.println(linkedMap); // {Java=59, JavaScript=50, Vue=36, Node=19}
-
HashTable 集合(最早期的双列集合)implements Map<K, V> 接口
- 键和值都不允许为 null,但 HashMap 和 LinkedHashMap 允许为 null
- 是同步的(单线程的,速度较慢)HashTable 被 HashMap 取代了 vector 被 ArrayList 取代了
- Properties 集合是唯一一个和 IO 流结合的集合(现在用的很活跃)
- Properties 集合的父类是 HashTable
Hashtable<String, String> map = new Hashtable<>(); map.put(null, ""); // 抛空指针异常
-
练习:计算一个字符串中每个字符出现的次数
HashMap<Character, Integer> map = new HashMap<>(); String str = new Scanner(System.in).next(); // 输入 159159++ char[]charArray = str.toCharArray(); // 将字符串转为字符数组 for (int i = 0; i < charArray.length; i++) { if(map.containsKey(charArray[i])) { // 判断 map 集合是否已经存在 key map.put(charArray[i], map.get(charArray[i]) + 1); } else { // 第一次出现的 字符 map.put(charArray[i], 1); } } System.out.println(map); // {1=2, 5=2, 9=2, +=2}
2.2.5. JDK9 对集合添加的优化
-
List接口,Set 接口,Map 接口:里面增加了一个静态的方法,可以给集合一次性添加多个元素
- 使用前提:当集合中存储的元素个数已经确定了,不会改变时使用
- 注意:
- of 方法只适合用于 List接口,Set 接口,Map 接口,不适合于接口的实现类
- of 方法的返回值是一个不能改变的集合,该集合不能使用 add / put 方法添加元素,如果加会抛异常
- Set 接口和 Map 接口在调用 of 方法时候,不能有重复的元素,否则会抛异常
List<? extends Serializable> list = List.of('a', "b", "c"); System.out.println(list); // [a, b, c]
2.2.6. 斗地主升级版
-
看牌时是排好序的
// 准备牌 HashMap<Integer, String> poker = new HashMap<>(); // 存储牌的索引 ArrayList<Integer> pokerIndex = new ArrayList<>(); List<String> colors = List.of("♠", "♥", "♣", "♦"); List<String> numbers = List.of("2", "A", "k", "Q", "J", "10", "9", "8", "7", "6", "5", "4", "3"); // 先将大王小王存在 poker 中 poker.put(0, "大王"); pokerIndex.add(0); poker.put(1, "小王"); pokerIndex.add(1); int index = 2; // 存储剩余 52 张牌 for (String number : numbers) { for (String color : colors) { poker.put(index, number); pokerIndex.add(index); index++; } } // 洗牌 Collections.shuffle(pokerIndex); // 存储三个玩家和底牌的索引 ArrayList<Integer> play1 = new ArrayList<>(); ArrayList<Integer> play2 = new ArrayList<>(); ArrayList<Integer> play3 = new ArrayList<>(); ArrayList<Integer> diPai = new ArrayList<>(); for (int i = 0; i < pokerIndex.size(); i++) { Integer in = pokerIndex.get(i); if(i >= 51) { diPai.add(in); } else { if(i % 3 ==0) { play1.add(in); } else if(i % 3 == 1) { play2.add(in); } else if(i % 3 == 2) { play3.add(in); } } } // 将牌拍好序 Collections.sort(play1); Collections.sort(play2); Collections.sort(play3); Collections.sort(diPai); // 看牌 // bright:2 2 A k Q 10 10 9 9 8 7 6 5 5 4 4 3 lookPoker("bright", poker, play1); // tom:大王 小王 2 A A A k k Q J J 10 8 7 7 6 5 lookPoker("tom", poker, play2); // lucy:2 k Q Q 10 9 9 8 7 6 6 5 4 4 3 3 3 lookPoker("lucy", poker, play3); // J J 8 lookPoker("diPai", poker, diPai); } // 看牌 public static void lookPoker(String name, HashMap<Integer, String> poker, ArrayList<Integer> list) { System.out.print(name + ":"); for (Integer key : list) { System.out.print(poker.get(key) + " "); } System.out.println(); }
2.3. 迭代器
-
迭代:即 Collection 集合元素的通用获取方式。在取出元素之前要判断集合中有没有元素,如果有就把这个元素取出来,继续循环判断…
-
Iterator 接口(两个常用方法
- boolean hasNext():如果仍有元素可以迭代,则返回 true
- E next():返回迭代的下一个元素(取出集合中的下一个元素)
-
获取迭代器:使用 Collection 接口中的方法 iterator(),该方法就是迭代器的实现类对象
Collection<String> coll = new ArrayList<>(); coll.add("Java"); coll.add("JavaScript"); coll.add("Php"); coll.add("Node.js"); coll.add("Vue.js"); Iterator<String> iter = coll.iterator(); while(iter.hasNext()) { String item = iter.next(); System.out.print(item + " "); // Java JavaScript Php Node.js Vue.js }
-
增强 for循环
- 底层使用的是迭代器,使用 for 的格式,简化了迭代器的书写
- 用来遍历数组和集合
// 格式:for(集合/数组 的数据类型 变量名 : 集合名/数组名) Collection<String> coll = new ArrayList<>(); coll.add("Java"); coll.add("JavaScript"); for(String item : coll) { System.out.print(item + " "); // Java JavaScript }
2.4. 可变参数
-
使用前提(JDK1.5之后出现的新特性)
- 当方法的 参数列表数据类型已经确定,但是参数的个数不确定
- 注意事项
- 一个方法的参数列表只能有一个可变参数
- 如果方法的参数有多个,那么可变参数必须写在参数列表的末尾
- 使用格式
- 修饰符 返回值类型 方法名(数据类型…变量名)
- 可变参数的原理
- 底层是一个数组,根据传递的参数个数不同,会创建不同长度的数组,来存储这些参数
// 计算 0-n 的和 public static int add(int...arr) { System.out.println(arr); // [I@723279cf 底层是一个数组 int sum = 0; for (int i = 0; i < arr.length; i++) { sum += arr[i]; } return sum; } // 调用 System.out.println(add(12,20)); // 32
- 可变参数的终极写法:(Object…obj)
2.5. Collections 集合工具类
-
常用的功能
- public static boolean addAll(Collection c, T… elements):往集合中添加一些元素
- public static void shuffle(List<?> list):打乱集合顺序
- public static void sort(List list):将集合中元素按照默认规则排序
ArrayList<String> list = new ArrayList<>(); // 一次性往集合中添加多个元素 Collections.addAll(list, "Java", "JavaScript", "Php"); // 打乱集合中的顺序 Collections.shuffle(list); // [Php, Java, JavaScript] System.out.println(list); // 按照默认升序进行排序 Collections.sort(list); // [Java, JavaScript, Php] System.out.println(list);
-
public static void sort(List list, Comparator<? super T>)
- 如果传的参数是自定义类型,必须实现 Comparable接口,重写接口中的方法 CompareTo
- Comparable 接口的排序规则
- this.排序的字段 - 参数.排序的字段:升序,反之就降序
ArrayList<Person> lPerson = new ArrayList<>(); lPerson.add(new Person("Java", 59)); lPerson.add(new Person("JavaScript", 56)); lPerson.add(new Person("Php", 39)); Collections.sort(lPerson); // [Person{name='Php', age=39}, Person{name='JavaScript', age=56}, Person{name='Java', age=59}] System.out.println(lPerson); public class Person implements Comparable<Person> { private String name; private int age; public Person() { } public Person(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } public int getAge() { return age; } // 重写排序的规则 @Override public int compareTo(Person o) { // return 0; 认为元素都是相同的 // 按照人的年龄升序进行排序 return this.getAge() - o.getAge(); } }
-
Comparator 和 Comparable 的区别
- Comparable:自己(this)和别人比较,自己需要实现 Comparable 接口,重写CompareTo 方法
- Comparator:相当于找一个第三方的裁判,比较两个
-
Comparator 的排序规则:o1 - o2 升序 o2 - o1 降序
- public static void sort(List list, Comparator<? super T>)
ArrayList<Integer> list = new ArrayList<>(); list.add(10); list.add(30); list.add(19); System.out.println(list); // [10, 30, 19] Collections.sort(list, new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o1 - o2; // 升序 // return o2 - o1; 降序 } }); System.out.println(list); // [10, 19, 30]
-
Comparator 比较自定义类型
ArrayList<Student> list = new ArrayList<>(); list.add(new Student("Java", 59)); list.add(new Student("JavaScript", 59)); list.add(new Student("Php", 39)); Collections.sort(list, new Comparator<Student>() { @Override public int compare(Student o1, Student o2) { int res = o1.getAge() - o2.getAge(); // 如果两个人年龄相同, 再使用姓名的第一个字母 if(res == 0) { res = o1.getName().charAt(0) - o2.getName().charAt(0); } return res; } }); // [Student{name='Php', age=39}, Student{name='Java', age=59}, Student{name='JavaScript', age=59}] System.out.println(list);
2.6. 多线程
-
并发与并行
-
并发:指两个或多个事件在同一个时间段内发生(交替执行)
-
并行:指两个或多个事件在同一时刻发生(同时执行,要快一些)
-
-
主线程:执行主(main)方法的线程
- 单线程程序:java 程序中只有一个线程,执行 main 方法开始,从上到下依次执行
-
创建多线程程序的方式一(继承 Thread 类)
-
步骤
-
创建一个 Thread 类的子类
-
在 Thread 的子类中重写 Thead 类中的 run 方法,设置线程任务
-
创建 Thread 类的子类对象
-
调用 Thread 类中的方法 start() 方法,开启新的线程,执行 run 方法
- void start() 使该线程开始执行;JVM 调用该线程的 run 方法
- 结果是两个线程并发地运行;当前线程(main 线程)和另一个线程(创建的线程执行其方法)
- 多次启动一个线程是非法的,特别是当线程已经结束执行后不能再重新启动
-
// 创建 Thread 类的子类 public class ThreadDemo01 extends Thread { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println("run => " + i); // 与 main 中的 for 是随机打印的 } } } ThreadDemo01 td = new ThreadDemo01(); td.start(); // 启动 ThreadDemo01 线程 for (int i = 0; i < 10; i++) { System.out.println("main => " + i); }
-
-
Thread 类
- 构造方法
- public Thread():分配一个新的线程对象
- public Thread(String name):分配一个指定名称的新的线程对象
- public Thread(Runnable target):分配一个带有指定目标新的线程对象
- public Thread(Runnable targer, String name):分配一个带有指定目标新的线程并指定名称
- 常用方法
- public String getName():获取当前线程名称
- public void start():线程开始执行, JVM 调用此线程的 run 方法
- public void run():此线程要执行的任务在此定义代码
- public static void sleep(long millis):使当前正在执行的线程以指定的毫秒暂停
- public static Thread currentThread():返回对当前正在执行的线程对象的引用
public ThreadDemo01(String name) { super(name); } ThreadDemo01 td = new ThreadDemo01("hello"); td.start(); System.out.println(td.getName()); // hello System.out.println(td.currentThread().getName()); // main td.setName("Java"); // 设置线程名称 System.out.println(td.getName()); // Java td.sleep(1000); // 线程休眠一秒钟
- 构造方法
-
创建多线程程序的方式二(实现 Runnable 实现类)
- 步骤
- 创建一个 Runnable 接口的实现类
- 在实现类中重写 Runnable 接口的 run 方法,设置线程任务
- 创建一个 Runnable 接口的实现类对象
- 创建 Thread 类对象,构造方法中传递 Runnable 接口的实现类对象
- 调用 Thread 类中的 start 方法,开启新的线程运行 run 方法
// 创建实现类 public class RunnableImpl implements Runnable { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + " => " + i); } } } RunnableImpl run = new RunnableImpl(); Thread t = new Thread(run); t.start(); for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + " => " + i); }
- 步骤
-
Thread 和 Runnable 的区别
- 实现 Runnable 接口的好处
- 避免了单继承的局限性(一个类只能继承一个类)
- 增强了程序的可扩展性,降低了程序的耦合性(解耦)
- 实现 Runnable 接口的方式把设置线程任务和开启新线程进行了分离(解耦)
- 实现类中重写了 run 方法:用来设置线程任务
- 创建 Thread 类对象,调用 start 方法:用来开启新线程
- 实现 Runnable 接口的好处
-
匿名内部类方式实现线程的创建
- 把子类继承父类,重写父类的方法,创建子类对象合一步完成
new Thread() { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + " ---> Thread"); } } }.start(); new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + " --> Runnable"); } } }).start();
-
下面是线程安全问题案例
- 会出现购买票重复问题和不存在的票
//实现卖票任务 public class RunnableImpl implements Runnable { // 定义一个多个线程共享的票源 private int ticket = 100; @Override public void run() { // 使用死循环, 让卖票功能操作重复执行 while (true) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } // 判断票源是否还有 if (ticket > 0) { // 票还有 System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "票"); ticket--; } else { break; } } } } // 测试 // 模拟买票案例 RunnableImpl run = new RunnableImpl(); // 创建三个线程 Thread t1 = new Thread(run); Thread t2 = new Thread(run); Thread t3 = new Thread(run); // 开启三个线程 t1.start(); t2.start(); t3.start();
2.6.1. 线程的同步(解决线程安全问题)
-
同步代码块
- 解决线程安全问题
public class RunnableImpl implements Runnable { // 定义一个多个线程共享的票源 private int ticket = 100; // 创建一个锁对象 Object obj = new Object(); @Override public void run() { // 使用死循环, 让卖票功能操作重复执行 while (true) { synchronized (obj) { // 判断票源是否还有 if (ticket > 0) { // 票还有 try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "票"); ticket--; } } } } }
-
同步方法
- 使用synchronized 修饰的方法,就叫做同步方法,保证 A 线程执行该方法时只能在方法外等着
public void run() { // 使用死循环, 让卖票功能操作重复执行 while (true) { payTicket(); } } public /* 使用了 synchronized 关键字 下面不需要加synchronized (this) */ void payTicket() { synchronized (this) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } // 判断票源是否还有 if (ticket > 0) { // 票还有 System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "票"); ticket--; } } }
-
静态方法(也可以保证安全问题)
private static int ticket = 100; public static /*synchronized*/ void payTicket() { synchronized (Runnable.class) { // 这儿不是 this 而是 Runnable.class try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } // 判断票源是否还有 if (ticket > 0) { // 票还有 System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "票"); ticket--; } } }
2.6.2. 解决线程安全问题(lock)
-
解决线程安全问题的三种方案:使用 lock 锁
- java.util.concurrent.locks.lock 接口
- lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作
- lock 接口的方法:
- void lock():锁住
- void unlock():解锁
- 使用步骤
- 在成员位置创建一个 ReentrantLock implement lock 接口
- 在可能会出现安全问题的代码前调用 lock 方法
- 在可能出现安全问题之后调用 unlock 方法
// 创建一个 ReentrantLock Lock l = new ReentrantLock(); @Override public void run() { // 使用死循环, 让卖票功能操作重复执行 while (true) { l.lock(); try { Thread.sleep(100); // 判断票源是否还有 if (ticket > 0) { // 票还有 System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "票"); ticket--; } } catch (InterruptedException e) { e.printStackTrace(); } finally { // 无论是否会出现异常, 都会释放锁 l.unlock(); } } }
2.6.3. 线程的等待与唤醒
-
调用 wait 和 notify 方法需要注意的细节
- wait 方法 与 notify 方法必须要由一个锁对象调用,原因:对应锁的对象可以通过 notify 唤醒使用同一个锁对象调用的 wait 方法的线程
- wait 方法与 notify 方法是属于 Object 类的方法的,原因:锁对象可以是任意对象,而任意对象的所属类都是继承了 Object 类的
- wait 方法 与 notify 方法必须在同步代码块或同步函数中使用,原因:必须通过锁对象调用这 2 个方法
-
等待与唤醒案例(做包子与吃包子案例)
Object obj = new Object(); // 创建顾客线程 new Thread() { @Override public void run() { // 保证等待和唤醒的线程只能有一个执行 while(true) { synchronized (obj) { System.out.println("告知老板的包子的数量"); try { obj.wait(); } catch (InterruptedException e) { e.printStackTrace(); } // 唤醒之后执行的代码 System.out.println("包子好了, 顾客准备开吃"); System.out.println("================="); } } } }.start(); // 创建老板线程 new Thread() { @Override public void run() { while(true) { try { Thread.sleep(1000); // 2 秒钟做好包子 } catch (InterruptedException e) { e.printStackTrace(); } synchronized (obj) { System.out.println("老板1秒钟之后做好包子,可以吃包子了"); obj.notify(); } } } }.start();
-
唤醒的方法
- void notify():唤醒在此对象监视器上等待的单个线程
- void notifyAll():唤醒在此对象监视器上等待的所有线程
obj.notifyAll();
-
吃货与包子铺的故事案例
- 包子类
public class BaoZi { public String pi; public String xian; // 包子的状态 boolean flag = false; }
- 包子铺类
public class BaoZiPu extends Thread { private BaoZi bz; public BaoZiPu(BaoZi bz) { this.bz = bz; } @Override public void run() { int count = 0; while(true) { synchronized (bz) { if(bz.flag) { // 有包子 try { bz.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } // 被唤醒之后执行, 包子铺生产包子 if(count % 2 ==0) { // 生产薄皮三鲜馅包子 bz.pi = "薄皮"; bz.xian = "三鲜馅"; } else { // 生产冰皮牛肉大葱包子 bz.pi = "冰皮"; bz.xian = "牛肉大葱"; } count++; System.out.println("包子铺正在生产:" + bz.pi + bz.xian + "包子"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } // 生产后修改包子的状态为 true bz.flag = true; // 唤醒吃货线程吃包子 bz.notify(); System.out.println("包子铺已经生产好了:" + bz.pi + bz.xian + "包子,吃货可以开吃了..."); } } } }
- 吃货类
public class CiHuo extends Thread { private BaoZi bz; public CiHuo(BaoZi bz) { this.bz = bz; } @Override public void run() { while(true) { synchronized (bz) { if(!bz.flag) { try { bz.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } // 被唤醒之后就吃包子 System.out.println("吃货正在吃:" + bz.pi + bz.xian + "包子"); // 吃货吃完了 bz.flag = false; // 唤醒包子铺生产 bz.notify(); System.out.println("吃货已经把" + bz.pi + bz.xian + "包子吃完了"); System.out.println("===================="); } } } }
- 测试
// 创建包子对象 BaoZi bz = new BaoZi(); // 创建包子铺线程, 开启生产包子 new BaoZiPu(bz).start(); // 创建吃货线程, 吃货吃包子 new CiHuo(bz).start();
-
线程池带来的好处
- 降低资源消耗。减少了创建和销毁线程的次数,每个线程都可以被重复利用,可执行多个任务
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行
- 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目
-
线程池的代码实现
- java.util.concurrent.Executors:线程池的工厂类,用来生成线程池
- static ExecutorService newFixedThreadPool(int nThreads):创建一个可重用固定线程数的线程池
- 使用步骤
- 使用线程池的工厂类 Executors 里面提供的静态方法 newFixedThreadPool 生产一个指定线程数量的线程池
- 创建一个类,实现 Runnable 接口,重写 run 方法,设置线程任务
- 调用 ExecutorService 中的方法 submit,传递线程任务(实现类),开启线程
- 调用 ExecutorSeervice 中的方法 shutdown 销毁线程池(不建议执行)
- java.util.concurrent.Executors:线程池的工厂类,用来生成线程池
public class RunnableImpl implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "创建了一个新的线程");
}
}
// 使用线程池
ExecutorService es = Executors.newFixedThreadPool(2);
// 线程池会一直开启, 使用完了线程会自动将线程归还给线程池继续使用
es.submit(new RunnableImpl());
es.submit(new RunnableImpl());
es.submit(new RunnableImpl());
es.submit(new RunnableImpl());
es.shutdown(); // 不建议使用(会将线程池销毁)
2.7. Lambda 表达式
-
在 Runnable 中的使用
- 由一些参数,一个箭头,一段代码组成
// Lambda 表达式标准格式 (参数类型 参数名称) -> { 代码语句 } new Thread(() -> System.out.println("run2...执行了")).start();
-
无参数返回值的练习
public interface Cook { void makeFood(); } /* // 不使用 Lamada 表达式 invokeCook(new Cook() { @Override public void makeFood() { System.out.println("吃饭了"); } }); */ // Lambda 表达式 invokeCook(() -> System.out.println("吃饭了")); public static void invokeCook(Cook cook) { cook.makeFood(); }
-
Lambda 表达式有参数有返回值的练习
// 按照年龄进行排序 Arrays.sort(arr, (o1, o2) -> o1.getAge() - o2.getAge());
-
Lambda 表达式有参数有返回值的接口
public interface Calculator { int calc(int a, int b); } public static void invokeCalc(int a, int b, Calculator c) { int sum = a + b; System.out.println(sum); } invokeCalc(19, 201, (a, b) -> a + b);
-
Lambda 省略格式 Lambda 使用前提
- Lambda 表达式:是可推导,可以省略
- 凡是根据上下文推导出来的内容都可以省略
- 可以省略的内容
- (参数列表):括号中的参数列表可以省略不写
- (参数列表):括号中的参数如果只有一个,那么类型和()都可以省略
- (一些代码):如果 {} 中的代码只有一行,无论是否有返回值,都可以省略 {} 和 return
invokeCalc(19, 201, (a, b) -> a + b);
- Lambda 的使用前提
- 使用 Lambda 必须是接口,且要求接口中有且仅有一个抽象方法
- 无论是 JDK 内置的 Runnable、Comparator 接口还是自定义的接口,只有当接口中的抽象方法存在唯一时,才可以使用 Lambda
- 使用 Lambda 必须具有上下文推断,也就是方法的参数或局部变量类型必须为 Lambda 对应的接口类型才能使用 Lambda 作为该接口的实例
2.8. IO 流
2.8.1. File 类
-
File 类的静态方法
- static String pathSeparator:与系统相关的路径分隔符
- static char pathSeparatorChar:与系统相关的路径分隔符
- static String Separator:与系统有关的默认名称分隔符
- static char SeparatorChar:与系统有关的默认名称分隔符
String ps = File.pathSeparator; System.out.println(ps); // 路径分隔符 windows:分号(;) linux:冒号{:} String s = File.separator; System.out.println(s); // \
-
File 类的构造方法
- File(String path):字符串的路径名称
- File(File parent, String child):根据 parent 路径和 child 路径创建一个新的 File实例
- 父路径与子路径可以单独写,使用起来比较灵活;父路径和子路径都可以改变
- 父路径是 File 类型,可以使用 File 的方法对路径进行一些操作,再使用路径创建对象
File f1 = new File("1.txt"); System.out.println(f1); // 1.txt File f2 = new File("C://", "2.txt"); System.out.println(f2); // C:\2.txt File parent = new File("d://"); File f3 = new File(parent, "3.txt"); System.out.println(f3); // d:\3.txt
-
File 类获取功能的方法
- public String getAbsolutePath():返回此 File 的绝对路径
- public String getPath():将 File 转换为路径字符串
- public String getName():返回 File 表示的文件或目录的名称
- public long length():返回 File 表示的文件的长度
- 获取文件的大写小,以字节为单位,文件夹是没有大小之分的
File f1 = new File("com\\bright"); System.out.println(f1.getAbsolutePath()); // E:\javaProjects\JavaSE\basic-code\com\bright System.out.println(f1.getPath()); // com\bright // 在 File 类中 toString() 方法就是调用的 getPath() 方法 System.out.println(f1.toString()); // com\bright // 就是路径结尾的部分 System.out.println(f1.getName()); // bright File f2 = new File("D:\\node.exe"); System.out.println(f2.length()); // 56467104 System.out.println(f1.length()); // 0 文件夹是没有大小之分的, 所以为0
-
File 类判断功能的方法
- public boolean exists():判断文件或者目录是否存在
- public boolean isDictory():判断该路径是否为目录
- public boolean isFile():判断该路径是否为文件
File f1 = new File("E:\\javaProjects\\JavaSE\\basic-code\\basic02-code\\basic02-code.iml"); System.out.println(f1.exists()); // true System.out.println(f1.isDirectory()); // false System.out.println(f1.isFile()); // true
-
创建删除功能的方法
- public boolean createNewFile():当且仅当具有该名称的文件尚不存在时,创建一个新的空文件
- public boolean delete():删除 File 表示的文件或目录
- public boolean mkdir():创建目录,(只能创建单级文件夹)
- public boolean mkdirs():创建目录,包括任何必须但不存在的父目录(可以创建单级/多级文件夹)
// 如果 f1 不存在则返回 true,反之返回 false File f1 = new File("E:\\1.txt"); boolean nF = f1.createNewFile(); System.out.println(nF); // true // 如果 f2 存在则返回 false, 反之返回 true File f2 = new File("E:\\hello"); System.out.println(f2.mkdir()); // true // 创建多级文件夹 File f3 = new File("E:\\hello\\test"); System.out.println(f3.mkdirs()); // true // 删除文件或者目录 File f4 = new File("E:\\hello"); System.out.println(f4.delete()); // true
-
File 类遍历(文件夹)目录的功能
- public String[] list():返回一个 String 数组,表示该 File 目录中的所有子文件或者目录
- public File[] listFiles():返回一个 File 数组,表示该 File 目录中所有的子文件或者目录
File f1 = new File("E:\\整改"); String[] list = f1.list(); for (int i = 0; i < list.length; i++) { System.out.println(list[i]); } File[] filesList = f1.listFiles(); for (int i = 0; i < files.length; i++) { System.out.println(filesList[i]); }
-
计算 n 的阶乘
System.out.println(jieC(8)); // 40320 public static int jieC(int n) { if(n == 1) { return 1; } return n*jieC(n-1); }
-
递归打印多级目录
public static void getAllFile(File dir) { File[] files = dir.listFiles(); for (File file : files) { if(file.isDirectory()) { getAllFile(file); } else { System.out.println(file); } } } // 调用(打印 f1 下面的所有目录和文件夹 File f1 = new File("E:\\软件测试"); getAllFile(f1);
-
综合案例(文件搜索)
File f1 = new File("E:\\软件测试"); getDocxFile(f1); public static void getDocxFile(File dir) { File[] files = dir.listFiles(); for (File file : files) { if(file.isDirectory()) { getDocxFile(file); } else { if(file.getName().endsWith(".docx")) { System.out.println(file); } } } }
-
FileFilter 过滤器的原理和使用
-
File[] listFiles (FileFilter filter)
-
java.io.FileFilter接口:用于抽象路径名(File 对象)的过滤器
-
作用:过滤文件(File 对象)
-
抽象方法:用来过滤文件的方法
- File pathname:使用 ListFiles 方法遍历目录,得到的每一个文件对象
-
-
File[] listFiles(FilenameFilter filter)
- java.io.FilenameFilter接口:实现接口的类的实例可用于过滤器文件名
- 作用:用来过滤文件名称
- boolean accept(File dir, String name):测试指定文件是否应该包含在某一文件列表中
- 参数:
- File dir:构造方法中传递的被遍历的目录
- String name:使用 ListFiles 方法遍历目录,获取每一个文件/文件夹的名称
-
注意:两个过滤器接口没有实现类
public class FileFilterImpl implements FileFilter { @Override public boolean accept(File pathname) { return pathname.getName().endsWith(".docx") || pathname.isDirectory(); } } public static void getAllFile(File dir) { File[] files = dir.listFiles(new FileFilterImpl()); for (File file : files) { if(file.isDirectory()) { getAllFile(file); } else { System.out.println(file); } } } File f1 = new File("E:\\软件测试"); getAllFile(f1);
-
-
使用 Lambda 表达式优化(匿名内部类)
public static void getAllFile(File dir) { File[] files = dir.listFiles((ldi, name) -> new File(ldi, name).isDirectory() || name.endsWith("docx")); for (File file : files) { if(file.isDirectory()) { getAllFile(file); } else { System.out.println(file); } } } File f1 = new File("E:\\软件测试"); getAllFile(f1);
2.8.2. 字节输入输出流
2.8.2.1. 字节输出流
-
java.io.OutputStream 抽象类是表示字节输出流的所有类的超类,将指定的字节信息写出到目的地
-
字节输出流的子类基本共性方法
-
public void close():关闭输出流并释放系统资源(在完成流的操作后必须释放系统资源
-
public void flush():刷新输出流并强制缓冲的输出字节被写出
-
public void write(byte[] b):将 b.length 字节从指定的字节数组写入输出流
-
public void write(byte[] b, int off, int len):从指定的字节数组写入 len 字节,从偏移量 off 开始输出
-
public abstract void write(int b):将指定的字节输出流(写一个)
-
FileOutputStream (将内存中的数据写入到硬盘的文件中)
- 构造方法:
- FileOutputStream(String name)
- FileOutputStream(File file)
- 构造方法:
FileOutputStream fos = new FileOutputStream(".\\1.txt"); // fos.write(85); 写一个字节 byte[]bytes = {97,65,69,100}; // 一次性写多个字节 fos.write(bytes); /* byte[]bytes = {65, 66, 67, 68, 69}; fos.write(bytes, 1, 2); BC 从 1 索引开始写, 写 2 个 */ /* byte[] bytes = "Hello 你好啊!".getBytes(); 将字符串转为字节数组 fos.write(bytes); // Hello 你好啊! */ fos.close();
-
-
字节输出流的续写和换行
- 续写:
- FileOutputStream(String name, boolean append)
- FileOutputStream(File file, boolean append)
- 换行
- “\r\n”
FileOutputStream fos = new FileOutputStream(".\\1.txt", true);// 第二个参数为 true 就续写 byte[] bytes = "续写".getBytes(); // byte[] bytes = "换行\r\n".getBytes(); \r\n 换行 fos.write(bytes); // Hello 你好啊! fos.close();
- 续写:
2.8.2.2. 字节输入流
-
InputStream 类是抽象类,是所有字节输入流的超类
-
所有子类共性的方法
- int read():从输入流中读取数据的下一个字节(读一个字节)
- int read(byte[] b):从输入流中读取一定数量的字节,并将其存储在缓冲区数组 b 中
- void close():关闭输入流并释放系统资源
-
FileInputStream 字节输入流:将硬盘文件中的数据读取到内存中使用
-
构造方法
- FileInputStream(String name)
- FileInputStream(File file)
-
String 类的构造方法
- String(byte[] byte):把字节数组转换为字符串
- String(byte[] byte, int offset, int length):把字节数组的一部分转换为字符串
- offset:数组的开始索引,length:转换的字节个数
FileInputStream fis = new FileInputStream(".\\1.txt"); int len = -1; /* while((len = fis.read()) != -1) { System.out.println((char)len); } */ byte[]car = new byte[10]; while((len = fis.read(car)) != -1) { String str = new String(car, 0, len); System.out.println(str); } fis.close();
-
-
复制文件
FileInputStream fis = new FileInputStream(".\\1.txt"); FileOutputStream fos = new FileOutputStream("E:\\2.txt"); byte[] car = new byte[1024]; int len = -1; // 每次读取到的有效数据(字节) while((len = fis.read(car)) != -1) { fos.write(car, 0, len); // len 是每次读取到的数据(字节) } fos.close(); fis.close();
2.8.3. 字符输入输出流
- UTF-8:占 3 个字节 GBK:占 2 个字节
2.8.3.1. 字符输入流
-
Reader 类:字符输入流,是抽象类,是字符输入流的超类
-
共性的成员方法
- int read():读取单个字符并返回
- int read(char[] cbuf):一次读取多个字符,将字符读入数组
- void close():关闭流并释放系统资源
-
java.io.FileReader extends InputStreamReader extends Reader
-
FileReader:文件字符输入流
- 作用:把硬盘文件中的数据以字符的方式读取到内存中
- 构造方法:
- FileReader(String fileName)
- FileReader(File file)
FileReader fr = new FileReader(".\\1.txt"); char[] car = new char[1024]; int len = -1; while((len = fr.read(car)) != -1) { String str = new String(car, 0, len); System.out.print(str); } fr.close();
2.8.3.2. 字符输出流
-
java.io.Writer:字符输出流,是所有字符输出流的超类,是抽象类
-
共性的方法
- void write(int c):写入单个字符
- void write(char[] cbuf):写入字符数组
- abstract void write(char[] cbuf, int off, int len):写入字符数组的某一部分,off :开始索引,len:写入的个数
- void write(String str):写入字符串
- void write(String str, int off, int len):写入字符串的某一部分,off 字符串的开始索引,len 写入的字符个数
- void flush():刷新流的缓冲
- void close():关闭流,释放系统资源
-
java.io.FileWriter extends OutputStreamWriter extends Writer
-
FileWriter(文件输出流):将内存中的字符数据写入到指定的文件中
-
构造方法
- FileWriter(File file)
- FileWriter(String fileName)
FileWriter fw = new FileWriter(".\\2.txt"); FileReader fr = new FileReader(".\\1.txt"); int len = -1; char[] car = new char[1024]; while((len = fr.read(car)) != -1) { String str = new String(car, 0, len); System.out.println(str); fw.write(car); } fw.close(); fr.close();
-
flush 方法和 close 方法的区别
- flush:刷新缓冲区,流对象可以继续使用
- close:先刷新缓冲区,然后释放系统资源
2.8.4. Properties 集合
-
java.util.Properties 集合 extends HashTable<k, v> implements Map<k, v>
-
Properties 类表示了一个持久的属性集。 Properties 可以保存流中加载
-
Properties 集合是唯一与 IO 流相结合的集合
- 可以使用 Properties 集合中的方法 store,把集合中的临时数据,持久化到硬盘上存储
- 可以使用 Properties 集合中的方法 load,把硬盘中保存的文件(键值对)读取到集合中使用
- 属性列表中每个键及其对应的都是一个字符串
- Properties 集合有一些特有操作字符串的方法
- Object setProperty(String key, String value) 调用 HashTable 的方法 put
- ObjectgetProperty(String key) 通过 key 获取 value
- Set StringPropertiesNames() 返回属性列中的键集,相当于 keySet 方法
Properties prop = new Properties(); prop.setProperty("Java", "59"); prop.setProperty("JavaScript", "50"); prop.setProperty("Node", "16"); // prop.put(1, true); Set<String> set = prop.stringPropertyNames(); for (String key : set) { // Java => 59 JavaScript => 50 Node => 16 System.out.print(key + " => " + prop.getProperty(key) + " "); } // 把集合中的临时数据,持久化到硬盘上存储 FileWriter fw; try { fw = new FileWriter(".\\1.txt"); prop.store(fw, "save data"); fw.close(); } catch (IOException e) { e.printStackTrace(); } // 把硬盘中保存的文件(键值对)读取到 Properties 集合中使用 Properties prop = new Properties(); FileReader fr = new FileReader(".\\1.txt"); prop.load(fr); // {Java=59, JavaScript=50, Node=16} System.out.println(prop);
2.8.5. 缓冲流
2.8.5.1. 字节缓冲流
-
BufferedOutputStream:字节缓冲输出流
-
构造方法
- BufferedOutputStream(OutputStream out)
- BufferedOutputStream(OutputStream out, int size)
- OutputStream out:给 FileOutputStream 增加一个缓冲区,提高写入效率
- int size:指定缓冲流内部缓冲区的大小,不指定默认
-
BufferedInputStream:字节缓冲输入流
-
构造方法
- BufferedInputStream(InputStream in)
- BufferedInputStream(InputStream in, int size)
- InputStream in:给 FileInputStream 增加一个缓冲区,提高读取效率
- int size:指定缓冲流内部缓冲区的大小,不指定默认
long start = System.currentTimeMillis(); BufferedInputStream bis = new BufferedInputStream(new FileInputStream(".\\1.mp4")); BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(".\\2.mp4")); int len = -1; byte[] car = new byte[1024*60]; while((len = bis.read(car)) != -1) { bos.write(car, 0, len); } long end = System.currentTimeMillis(); System.out.println(end - start + "ms"); // 130ms bos.close(); bis.close();
2.8.5.2. 字符缓冲流
-
BufferedWriter:字符缓冲输出流(最后面需要 flush 一下)
-
构造方法
- BufferedWriter(Writer out)
- BufferedWriter(Writer out, int size)
- Writer out:给 FileWriter 增加一个缓冲区,提高写入效率
- int size:指定缓冲流内部缓冲区的大小,不指定默认
-
特有的成员方法
- void newLine():写入一个行分隔符,会根据不同的操作系统获取不同的行分隔符
BufferedWriter bw = new BufferedWriter(new FileWriter(".\\1.html")); bw.write("hello"); bw.newLine(); bw.write("java"); bw.flush();
-
BufferReader:字符缓冲输入流
-
构造方法
- public BufferedReader(Reader in)
- public BufferedWriter(Writer out)
-
特有的成员方法
- public String readLine():读取一行
String str = br.readLine(); System.out.println(str); // <!doctype html>
-
复制文件文件
long start = System.currentTimeMillis(); BufferedReader br = new BufferedReader(new FileReader(".\\node.html")); BufferedWriter bw = new BufferedWriter(new FileWriter(".\\1.html")); int len; char[] car = new char[1024*10]; while((len = br.read(car)) != -1) { String str = new String(car, 0, len); System.out.println(str); bw.write(car); } bw.close(); br.close(); long end = System.currentTimeMillis(); System.out.println(end - start + "ms"); // 49ms
-
对文本文件内容进行排序(对出师表内容)
// 用来存储每一行 HashMap<String, String> map = new HashMap<>(); BufferedReader br = new BufferedReader(new FileReader(".\\10.txt")); BufferedWriter bw = new BufferedWriter(new FileWriter(".\\11.txt")); // 每一行读取的数据 String line; while((line = br.readLine()) != null) { String[] arr = line.split("\\."); map.put(arr[0], arr[1]); } for (String key : map.keySet()) { String value = map.get(key); line = key + "." + value; bw.write(line); bw.newLine(); } // 释放资源 bw.close(); br.close();
2.8.6. 转换流
-
编码:字符(能看懂的)——字节(看不懂的)
-
解码:字节(看不懂的)——字符(能看懂的)
-
java.io.OutputStreamWriter extends Writer
- 是字符通向字节流的桥梁:可以指定的 charset 将要写入的字符编码成字节(编码)
- 构造方法
- OutputStreamWriter(OutputStream out):使用默认的字符编码
- OutputStreamWriter(OutputStream out, String charsetName):使用指定的字符编码
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(".\\1.txt"), "utf-8"); // 默认是 utf-8 编码 osw.write("你好"); osw.flush(); osw.close();
-
java.io.InputStreamReader extends Reader
- 是字节流通向字符流的桥梁:可以指定的 charset 读取字节并将其解码为字符
- 构造方法
- InputStreamReader(InputStream in):使用默认字符解码
- InputStreamReader(InputStream in, String charsetName):使用指定的字符解码
InputStreamReader isr = new InputStreamReader(new FileInputStream(".\\1.txt"), "utf-8"); // 默认是 utf-8 解码 int len = -1; while((len = isr.read()) != -1) { System.out.print((char)len); // 你好 } isr.close();
-
使用 将 gbk 的文件复制成 utf-8 格式的文件
InputStreamReader isr = new InputStreamReader(new FileInputStream(".\\1.txt"), "gbk"); OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(".\\2.txt")); int len = -1; char[] car = new char[1024]; while((len = isr.read(car)) != -1) { osw.write(car, 0, len); } osw.close(); isr.close();
2.8.7. 序列化与反序列化
-
序列化和反序列化的类必须实现 java.io.Serializable 接口(标记接口)
-
java.io.ObjectOutputStream extends OutputStream (序列化流) 序列化都是对象
- 把对象以流的方式写入到文件中保存
- 构造方法
- ObjectOutputStream(OutputStream out)
- 特有成员方法
- void writeObject(Object obj):将指定的对象写入 ObjectOutputStream
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(".\\3.txt")); oos.writeObject(new Person("JavaScript", 59)); oos.close();
-
java.io.ObjectInputStream(InputStream in) extends InputStream**(反序列化流)**
- 把文件中保存的对象,以流的方式读取出来使用
- 构造方法
- ObjectInputStream(InputStream in)
- 特有的成员方法
- Object readObject():从 ObjectInputStream 中读取对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(".\\3.txt")); Person o = (Person)ois.readObject(); ois.close(); System.out.println(o);
-
如果不需要将某一个字段被序列化,在修饰符后面加上 transient 瞬态关键字
private transient String name; Person{name='null', age=59} // name 字段不会被序列化
-
序列化之后,序列化的类发生了改变后,在反序列化会抛出异常(InvalidClassException)
- 为了解决这一个序列化号不一致的问题,可以自己定义序列化号
// 在被序列化的类中加入 private static final long serialVersionUID = 42L;
-
序列化集合(练习)
- 存储对象的集合
// 序列化 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(".\\3.txt")); LinkedList<Person> list = new LinkedList<>(); list.add(new Person("Java", 59)); list.add(new Person("JavaScript", 46)); list.add(new Person("Node", 20)); oos.writeObject(list); oos.close(); // 反序列化 ObjectInputStream ois = new ObjectInputStream(new FileInputStream(".\\3.txt")); LinkedList<Person> list = (LinkedList<Person>)ois.readObject(); ois.close(); System.out.println(list);
2.8.8. 打印流
-
java.io.PrintStream extends OutputStream:打印流
- PrintStream 特点
- 只负责数据的输出,不负责数据的读取
- PrintStream 永远不会抛出 IOException 异常
- 特有方法
- print,println
- 构造方法
- PrintStream(File file)
- PrintStream(OutputStream out)
- PrintStream(String fileName)
- 注意
- 如果使用继承自父类的 write 方法写数据,那么查看数据的时候会查询编码表 97 -> a
- 如果使用自己的特有方法 print / println 方法写数据,写的数据会原样输出
PrintStream ps = new PrintStream(".\\1.txt"); ps.write(97); // a ps.write(13); // 回车 ps.println("hello"); // hello ps.print(97); // 97 // 改变打印流的目的地, 一般是打印在控制台, 在这儿是打印到 1.txt 中 System.setOut(ps); System.out.println("我在打印流的 setOut 中"); ps.close();
- PrintStream 特点
2.9. 网络编程
-
TCP 通信的客户端代码实现
- TCP 通信客户端:向服务器发送连接请求,给服务器发送数据,读取服务器回写的数据
- 表示客户端的类
- java.net.Socket:该类实现客户端套接字,套接字(包含了 IP 地址和端口号的网路单位)是两台机器间通信的端点
- 构造方法
- Socket(String host, int port):创建一个流套接字并将其连接到指定主机上的指定端口号
- String host:服务器主机的名称 / 服务器的 IP 地址
- int port:服务器的端口号
- Socket(String host, int port):创建一个流套接字并将其连接到指定主机上的指定端口号
- 成员方法
- OutputStream getOutputStream():返回此套接字的输出流
- InputStream getInputStream():返回此套接字的输入流
- void close():关闭此套接字
- 注:
- 客户端和服务器端进行交互,必须使用 Socket 中提供的网络流,不能使用自己创建的流对象
- 创建客户端对象 Socket 时,就会去请求服务器经过 3 次握手建立连接通路
Socket socket = new Socket("127.0.0.1", 8989); OutputStream os = socket.getOutputStream(); os.write("你好服务器".getBytes()); InputStream is = socket.getInputStream(); byte[] car = new byte[1024]; int len = is.read(car); System.out.println(new String(car, 0, len)); socket.close();
-
TCP 通信的服务端代码实现
-
TCP 通信客户端:接收客户端的请求,读取客户端发送的数据,给客户端回写数据
-
表示服务器端的类
- java.net.ServerSocket:该类实现服务器套接字
-
构造方法
- ServerSocket(int port):创建绑定到特定端口的服务器套接字
-
服务端必须知道是哪台客户端请求的服务器,可以使用 accept 方法获取到请求的客户端对象 Socket
-
成员方法
- Socket accept():侦听并接受到该套接字的连接
ServerSocket server = new ServerSocket(8989); Socket socket = server.accept(); InputStream is = socket.getInputStream(); byte[] car = new byte[1024]; int len = is.read(car); System.out.println(new String(car, 0, len)); OutputStream os = socket.getOutputStream(); os.write("收到ok".getBytes()); socket.close(); server.close();
-
2.9.1. 文件上传
-
文件上传案例的客户端
-
文件上传案例阻塞问题
- 解决:上传完文件,给服务器写一个结束标记
- void shutdownOutput():禁用该套接字的输出流
- 对于 TCP 套接字,任何以前写入的数据都将被发送并且后跟 TCP 的正常连接终止序列
// 读取被上传的文件 BufferedInputStream bis = new BufferedInputStream(new FileInputStream(".\\1.mp4")); Socket socket = new Socket("127.0.0.1", 8989); // 发送给服务端的缓冲输出流 BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream()); int len = -1; byte[] car = new byte[1024]; while((len = bis.read(car)) != -1) { bos.write(car, 0, len); } // 给服务端写一个结束标记 socket.shutdownOutput(); // 读取服务端返回的数据 InputStream is = socket.getInputStream(); while((len = is.read(car)) != -1) { System.out.println(new String(car, 0, len)); } bis.close(); socket.close();
-
文件上传案例的客户端
// 开启服务器 ServerSocket server = new ServerSocket(8989); // 获取客户端的 Socket 对象 Socket socket = server.accept(); // 读取客户端的缓冲输入流 BufferedInputStream bis = new BufferedInputStream(socket.getInputStream()); // 将服务器端的数据写在硬盘上 BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(".\\2.mp4")); int len = -1; byte[] car = new byte[1024]; while((len = bis.read(car)) != -1) { bos.write(car, 0, len); } // 返回给客户端的输出流对象 OutputStream os = socket.getOutputStream(); os.write("上传完毕!".getBytes()); bos.close(); socket.close();
-
使用多线程提高效率
ServerSocket server = new ServerSocket(8989); while(true) { // 使用多线程技术提高效率(有一个客户端上传文件, 就开启一个线程 new Thread(() -> { try { Socket socket = server.accept(); BufferedInputStream bis = new BufferedInputStream(socket.getInputStream()); BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(".\\2.mp4")); int len = -1; byte[] car = new byte[1024]; while((len = bis.read(car)) != -1) { bos.write(car, 0, len); } OutputStream os = socket.getOutputStream(); os.write("上传完毕!".getBytes()); bos.close(); socket.close(); } catch (IOException e) { e.printStackTrace(); } }).start(); }
2.10. 函数式接口
2.10.1. 函数式接口基础
-
函数式接口:有且仅有一个抽象方法的接口
@FunctionalInterface // 检测接口是否是一个函数式接口 public interface MyFunction { void method(); } MyFunction my = () -> System.out.println("hello"); my.method(); // hello
-
使用 Lambda 优化日志案例(Lambda 延迟加载)
- Lambda 的使用前提:必须存在函数式接口
@FunctionalInterface // 检测接口是否是一个函数式接口 public interface MessageBuilder { // 定义一个拼接消息的抽象方法, 返回被拼接的消息 String builderMessage(); } String msg1 = "Hello"; String msg2 = "World"; String msg3 = "Java"; showLog(1, () -> msg1 + " " + msg2 + " " + msg3); public static void showLog(int level, MessageBuilder mb) { if(level == 1) { // 只有等级为 1 级, 才会执行拼接字符串 String msg = mb.builderMessage(); System.out.println(msg); } }
-
函数式接口作为方法的参数案例
startThread(() -> System.out.println(Thread.currentThread().getName() + "开启了")); public static void startThread(Runnable run) { new Thread(run).start(); // Thread-0开启了 }
-
函数式接口作为方法的返回值类型案例
String[] arr = { "Java", "JavaScript", "Node", "Vue" }; Arrays.sort(arr, getComparator()); System.out.println(Arrays.toString(arr)); // [Vue, Java, Node, JavaScript] private static Comparator<String> getComparator() { // return (o1, o2) -> o1.length() - o2.length(); return Comparator.comparingInt(String::length); // 比较字符串的长度 }
2.10.2. 常用的函数式接口
-
Supplier 接口:仅包含一个无参的方法:T get():用来获取一个泛型参数指定类型的对象数据
- Supplier 接口为生产型接口,指定接口的泛型是什么,get 方法就会生成对应的类型
String str = getString(() -> "Java"); System.out.println(str); // Java public static String getString(Supplier<String> sup) { return sup.get(); }
-
Supplier 接口练习_求数组元素的最大值
int[] arr = { 16, 10, 79, 75, 29, 21, 30 }; int arrMax = getMax(() -> { int max = arr[0]; for (int i = 1; i < arr.length; i++) { if (arr[i] > max) { max = arr[i]; } } return max; }); System.out.println(arrMax); // 79 public static int getMax(Supplier<Integer> sup) { return sup.get(); }
-
Consumer 接口:与 Supplier 接口相反,它是消费数据
- 只能有一个输入参数无输出的功能代码
// 消费方式:直接输出字符串 method("Java", (name) -> System.out.println(name)); // Java // 消费方式:将字符串进行反转 method("Java", (name) -> { String revName = new StringBuilder(name).reverse().toString(); System.out.println(revName); // avaJ }); public static void method(String name, Consumer<String> con) { con.accept(name); }
-
Consumer 接口的默认方法 andThen
- 作用:需要两个 Consumer 接口,可以把两个 Consumer 接口组合到一起,再对数据进行消费
// 连接两个 Consumer 接口 method("Java", (con1) -> { // 消费方式:将字符串转为大写输出 System.out.println(con1.toUpperCase()); }, (con2) -> { // 消费方式:将字符串转为小写输出 System.out.println(con2.toLowerCase()); }); public static void method(String s, Consumer<String> con1, Consumer<String> con2) { con1.andThen(con2).accept(s); // 先执行 con1 再 con2 }
-
Consumer 接口练习_格式化打印信息
String[] arr = { "Java, 59", "JavaScript, 36", "Node, 19" }; printInfo(arr, (msg) -> { // 消费方式:对字符串进行切割, 获取姓名 String name = msg.split(",")[0]; System.out.print("姓名:" + name + " "); }, (msg) -> { // 消费方式:对字符串进行切割, 获取年龄 String age = msg.split(",")[1]; System.out.println("年龄:" + age + "。"); }); /* 姓名:Java 年龄: 59。 姓名:JavaScript 年龄: 36。 姓名:Node 年龄: 19。 */ public static void printInfo(String[] arr, Consumer<String> con1, Consumer<String> con2) { for (String msg : arr) { con1.andThen(con2).accept(msg); } }
-
Predicate 接口:对某种类型的数据进行判断,返回值是 boolean
- 抽象方法:test
- 用于条件判断,固定返回布尔值
String str = "Java"; boolean b = checkStr(str, (s) -> s.length() < 5); System.out.println(b); // true public static boolean checkStr(String s, Predicate<String> pre) { return pre.test(s); }
-
Predicate 接口的默认方法 and(需要两个条件都为 true 才 true)
boolean b = checkStr("Java", (s) -> s.length() < 4, (s) -> s.contains("a")); System.out.println(b); // false public static boolean checkStr(String s, Predicate<String> pre1, Predicate<String> pre2) { return pre1.and(pre2).test(s); }
-
Predicate 接口的默认方法 or (需要两个条件至少其中一个为 true 才 true
boolean b = checkStr("Java", (s) -> s.length() < 4, (s) -> s.contains("a")); System.out.println(b); // true public static boolean checkStr(String s, Predicate<String> pre1, Predicate<String> pre2) { return pre1.or(pre2).test(s); }
-
Predicate 接口的默认方法 negate(需要条件为假才 true)
boolean b = checkStr("Java", (s) -> s.length() > 4); System.out.println(b); // true public static boolean checkStr(String s, Predicate<String> pre) { return pre.negate().test(s); }
-
Predicate 接口练习_集合信息筛选(姓名为 4 个字,性别为女的数据)
String[] arr = { "迪丽热巴,女", "古力娜扎,女", "胡歌,男", "赵丽颖,女", "周杰伦,男"}; ArrayList<String> list = filter(arr, (s) -> { String name = s.split(",")[0]; return name.length() == 4; }, (s) -> { String sex = s.split(",")[1]; return "女".equals(sex); }); System.out.println(list); // [迪丽热巴,女, 古力娜扎,女] public static ArrayList<String> filter(String[] arr, Predicate<String> pre1, Predicate<String> pre2) { ArrayList<String> list = new ArrayList<>(); for (String s : arr) { boolean b = pre1.and(pre2).test(s); if(b) { list.add(s); } } return list; }
-
Function<T, R> 接口:用来根据一个类型的数据得到另一个类型的数据,前者为前置条件,后者为后置条件
- 抽象方法:apply
- 只能有一个输入参数且需要返回数据的功能代码
change("1230", (s) -> Integer.parseInt(s)); // 前面的参数是要转换的类型, 后面的参数是转换成什么样的类型 public static void change(String s, Function<String, Integer> fun) { Integer in = fun.apply(s); System.out.println(in); // 1230 }
-
Function 接口的默认方法 andThen:用来组合操作
String str = change("1230", (s) -> Integer.parseInt(s) + 10, (s) -> s.toString()); System.out.println(str); // 1240 // 把字符串转为 Integer 再加一个数 最后再转换为字符串 public static String change(String s, Function<String, Integer> fun1, Function<Integer, String> fun2) { return fun1.andThen(fun2).apply(s); }
-
Function生成定长字符串(泛型的第一个参数是传入的参数类型,第二个参数是返回的参数类型)
Function<Integer, String> randStrFunc = l -> { String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; StringBuilder str = new StringBuilder(); Random random = new Random(); for(int i = 0; i < l; i++) { int position = random.nextInt(chars.length()); str.append(chars.charAt(position)); } return String.valueOf(str); }; String str = randStrFunc.apply(4); System.out.println(str);
2.11. Stream 流
2.11.1. 流的基本使用与获取
-
Stream 流属于管道流,只能消费一次,第一个流调用完毕之后就会流到下一个 Stream 上,之前那个流就不能再使用了
-
使用 Stream 流的方式遍历集合,对集合中的数据进行过滤再打印
-
Stream常用方法
- forEach 循环遍历
- map 映射每个元素到对应的结果
- filter 通过设置的条件过滤出元素
- limit 获取指定数量的流
- sorted 对流进行排序
- Collectors 将流转换成集合和聚合元素
List<String> list = new ArrayList<>(); list.add("张无忌"); list.add("周芷若"); list.add("赵敏"); list.add("张三丰"); list.add("张东"); list.stream().filter(name -> name.startsWith("张")) .filter(name -> name.length() == 3) .forEach(name -> System.out.print(name + " ")); // 张无忌 张三丰
-
获取流(java.util.stream.Stream 是 Java 8 新加入的最常用的流接口,不是函数式接口)
// 方式一:集合转换为流对象 List<String> list = new ArrayList<>(); Stream<String> stream1 = list.stream(); Set<String> set = new HashSet<>(); Stream<String> stream2 = set.stream(); Map<String, String> map = new HashMap<>(); Set<String> keySet = map.keySet(); Stream<String> stream3 = keySet.stream(); Collection<String> values = map.values(); Stream<String> stream4 = values.stream(); Set<Map.Entry<String, String>> entries = map.entrySet(); Stream<Map.Entry<String, String>> stream5 = entries.stream(); // 方式二:数组转换为流对象 Stream.of(...) Stream<Integer> stream6 = Stream.of(1, 2, 3, 4, 5); // 可变参数, 可以传数组 Integer[] arr = { 1, 2, 3, 4, 5 }; Stream<Integer> stream7 = Stream.of(arr);
2.11.2. Stream 流的常用方法
-
forEach 方法
- void forEach(Consumer<? super T> action):接收一个 Consumer 接口函数,会将每一个流对象交给该函数进行处理
- forEach 方法用来遍历流中的数据,是一个终结方法,遍历之后不能调用 Stream 流中的其他方法
Stream<String> stream = Stream.of("Java", "JavaScript", "Node", "Vue"); stream.forEach((language) -> System.out.print(language + " ")); // Java JavaScript Node Vue
-
filter 方法(返回值还是一个流对象)
- Stream filter(Predicate<? super T> predicate)
Stream<String> stream1 = Stream.of("Java", "JavaScript", "Node", "Vue"); Stream<String> stream2 = stream1.filter((language) -> language.startsWith("J")); stream2.forEach((language) -> System.out.print(language + " ")); // Java JavaScript
-
映射:map 方法(一种数据类型转为另一种类型
- Stream map(Function<? super T, ? extends R> mapper)
Stream<String> stream1 = Stream.of("19", "29", "17", "9"); Stream<Integer> stream2 = stream1.map((s) -> Integer.parseInt(s)); stream2.forEach((num) -> System.out.print(num + " ")); // 19 29 17 9
-
统计个数:count
- long count():用于统计 Stream 流中元素的个数,是一个终结方法
ArrayList<Integer> list = new ArrayList<>(); list.add(19); list.add(89); list.add(66); list.add(51); Stream<Integer> stream = list.stream(); System.out.println(stream.count()); // 4
-
取用前几个:limit 方法可以对流进行截取,只取用前几 n 个(延迟方法)
- Stream limit(long maxSize)
String[] arr = { "美羊羊", "喜羊羊", "沸羊羊", "小灰灰", "灰太狼" }; Stream<String> stream1 = Stream.of(arr); // 使用 limit 对 stream 进行截取 只取前 3 个元素 Stream<String> stream2 = stream1.limit(3); stream2.forEach(name -> System.out.print(name + " ")); // 美羊羊 喜羊羊 沸羊羊
-
跳过前几个:skip
- Stream skip(long n)
String[] arr = { "美羊羊", "喜羊羊", "沸羊羊", "小灰灰", "灰太狼" }; Stream<String> stream1 = Stream.of(arr); // 使用 limit 对 stream 进行截取 只取前 3 个元素 Stream<String> stream2 = stream1.skip(3); stream2.forEach(name -> System.out.print(name + " ")); // 小灰灰 灰太狼
-
组合:concat:将两个流合并为一个流
- static Stream concat(Stream<? extends T> a, Stream<? extends T> b)
String[] arr1 = { "美羊羊", "喜羊羊", "沸羊羊", "小灰灰", "灰太狼" }; String[] arr2 = { "Java", "JavaScript" }; Stream<String> stream1 = Stream.of(arr1); Stream<String> stream2 = Stream.of(arr2); Stream<String> stream3 = Stream.concat(stream2, stream1); stream3.forEach(name -> System.out.print(name + " ")); // Java JavaScript 美羊羊 喜羊羊 沸羊羊 小灰灰 灰太狼
2.12. 方法引用
-
通过对象名引用成员方法
public class MethodRef { public void printUpperCaseStr(String str) { System.out.println(str.toUpperCase()); // JAVASCRIPT } } // 对象名引用成员方法 MethodRef mr = new MethodRef(); printStr(mr::printUpperCaseStr); public static void printStr(PrintTable pt) { pt.print("JavaScript"); }}
-
通过类名引用静态成员方法
@FunctionalInterface public interface Calculate { int calcAbs(int num); } int num = method(-10, Math::abs); System.out.println(num); public static int method(int num, Calculate c) { return c.calcAbs(num); }
-
通过 super 引用父类的成员变量
public interface Greet { void greet(); } // 父类 public class Human { public void sayHello() { System.out.println("Hello, 我是 Human!"); // Hello, 我是 Human! } } public class Man extends Human { public void sayHello() { System.out.println("Hello, 我是 man!"); } public void method(Greet g) { g.greet(); } public void show() { method(super::sayHello); // 引用父类的 sayHello } } new Man().show();
-
通过 this 引用本类的成员方法
@FunctionalInterface public interface Richer { void buy(); } public class Husband { public void buyHouse() { System.out.println("在北京二环内买一套四合院!"); // 在北京二环内买一套四合院! } public void marry(Richer r) { r.buy(); } public void soHappy() { marry(this::buyHouse); } public static void main(String[] args) { new Husband().soHappy(); } }
-
类的构造器引用
@FunctionalInterface public interface PersonBuilder { // 根据传递的姓名,创建 Person 对象返回 Person builderPerson(String name); } public static void printName(String name, PersonBuilder pb) { Person p = pb.builderPerson(name); System.out.println(p.getName()); // 迪丽热巴 } printName("迪丽热巴", Person::new);
-
数组的构造器引用
@FunctionalInterface public interface ArrayBuilder { int[] builderArray(int length); } public static int[] createArray(int length, ArrayBuilder ab) { return ab.builderArray(length); } int[] arr = createArray(5, int[]::new); System.out.println(Arrays.toString(arr)); // [0, 0, 0, 0, 0] System.out.println(arr.length); // 5
2.13. Junit 测试
- 一般会使用断言操作来处理结果
- Assert.assertEquals(期望的值, 运算的结果)
- @Before:被修饰的方法会在测试方法之前被自动执行(一般申请资源用)
- @After:被修饰的方法会在测试方法之后被自动执行(一般用于释放资源)
2.14. 反射 --> 框架设计的灵魂
-
框架:半成品软件。可以在框架的基础上进行软件开发,简化代码
-
反射:在运行时动态访问类与对象的技术
- JDK1.2版本后的高级特性,属于 java.lang.reflect
- 大多数框架都基于反射实现参数配置、动态注入等特性
-
好处
- 可以在程序运行过程中操作这些对象
- 可以解耦,提高程序的可扩展性
-
反射的核心类
- Class 类
- class.forName() 方法将指定的类加载到JVM中
- classObj.newInstance() 通过默认构造方法创建新的对象。classObj代表class对象
- classObj.getConstructor() 获取指定的public修饰构造方法Constructor对象
- classObj.getMethod() 获取指定的public修饰方法Method对象
- classObj.getField() 获取指定的public修饰成员变量Fileld对象
- Constructor 构造方法类
- classObj.getConstructor() 获取指定public修饰的构造方法对象
- constructorObj.newInstance() 通过对应的构造方法创建对象
// 利用带参构造方法创建对象 Constructor constr = Class.forName("...").getConstructor(new Class[]{Integer.class, String.class...}); 类 对象 = (类) constr.newInstance(new Object[]{100,"李磊"...);
- Method 方法类
- classObj.getMethod() 获取指定public 修饰的方法对象
- methodObj.invoke() 调用指定对象的对应方法
Class employeeClass = Class.forName("com.imooc.reflect.entity.Employee"); // 得到方法对象,updateSalary方法名称,后面是为了解决重载的问题 Method updateSalaryMethod = employeeClass.getMethod("updateSalary" , new Class[]{ Float.class}); // 第一个参数传入employee对象,第二个参数传入对应的参数 (返回的是employee对象)Employee employee1 = (Employee)updateSalaryMethod.invoke(employee,new Object[]{1000f});
- Field 成员变量类
- classObj.getField 获取指定public修饰的成员变量对象
- fieldObj.set() 为某对象指定成员变量赋值
- fieldObj.get() 获取对象指定成员变量取值
// 将Employee加载到JVM中 Class employeeClass = Class.forName("com.imooc.reflect.entity.Employee"); // 实例化对象 Constructor constructor = employeeClass.getConstructor(new Class[]{ Integer.class,String.class,Float.class,String.class }); Employee employee = (Employee) constructor.newInstance(new Object[]{ 100,"李磊",3000f,"研发部" }); // 拿到属性ename Field enameField = employeeClass.getField("ename"); enameField.set(employee,"李雷"); // 设置值, 第一个参数传入对象, 第二个参数传入设置的值 String ename = (String)enameField.get(employee); // 传入对象
- Class 类
-
getDeclared系列方法(可以获取被private修饰的构造方法、成员方法、成员变量)
- getDeclaredConstructor(s)|Method(s)|Field(s) 获取对应的对象
Class employeeClass = Class.forName("com.imooc.reflect.entity.Employee"); Constructor constructor = employeeClass.getConstructor(new Class[]{ Integer.class, String.class, Float.class, String.class }); Employee employee = (Employee) constructor.newInstance(new Object[]{ 100, "李磊", 3000f, "研发部" }); //获取当前类所有成员变量 Field[] fields = employeeClass.getDeclaredFields(); for(Field field : fields){ if(field.getModifiers() == 1){ //pubilc修饰 Object val = field.get(employee); System.out.println(field.getName() + ":" + val); } else if(field.getModifiers() == 2){ //private修饰 if(field.getName().equals("dname")) { field.setAccessible(true); // 允许修改被private修饰的成员变量dname field.set(employee, "开发部"); } // 方法的get后面以大写开头,所以有字符串截取 substring String methodName = "get" + field.getName().substring(0,1).toUpperCase() + field.getName().substring(1); Method getMethod = employeeClass.getMethod(methodName); Object ret = getMethod.invoke(employee); System.out.println(field.getName() + ":" + ret); } }
-
获取 Class 对象的方式
- Class.forName(“全类名”):将字节码文件加载进内存,返回 Class 对象
- 多用于配置文件,将类名定义在配置文件中,读取文件加载类
- 类名.class:通过类名的属性 class 获取
- 多用于参数的传递
- 对象.getClass() 方法在 Object 类中定义着
- 多用于对象获取字节码对象
Class<?> cls1 = Class.forName("com.bright.domain.Person"); System.out.println(cls1); Class<Person> cls2 = Person.class; System.out.println(cls2); Person p = new Person(); Class<? extends Person> cls3 = p.getClass(); System.out.println(cls3); System.out.println(cls1 == cls2); // true System.out.println(cls1 == cls3); // true
- Class.forName(“全类名”):将字节码文件加载进内存,返回 Class 对象
-
结论:同一个字节码文件(*.class)在一次程序运行过程中,只会被加载一次,不论通过哪一种方式获取的 Class 对象
-
获取功能:
- 获取成员变量们
- Field[] getFields() :获取所有 public 修饰的成员变量
- Field getField(String name):获取指定名称的成员变量(被 public 修饰)
- Field[] getDeclaredFields():获取所有的成员变量,不考虑修饰符
- Field getDeclaredField(String name):获取指定名称的成员变量,不考虑修饰符
// 获取 Person 类的字节码文件 Class<Person> cls = Person.class; // 获取所有的成员变量们(只能获取被 public 修饰的) Field[] fields = cls.getFields(); System.out.println(Arrays.toString(fields)); // [] // 获取成员变量 sex Field sex = cls.getField("sex"); Person p = new Person(); // 给 sex 设置值 sex.set(p, "男"); Object value = sex.get(p); System.out.println(value); // 男 // 获取所有的成员变量, 不考虑修饰符 Field f2 = cls.getDeclaredField("name"); // 为了解决 IllegalAccessException, 下面的暴力反射 f2.setAccessible(true); System.out.println(f2.get(p)); // null System.out.println(f2); // private java.lang.String com.bright.domain.Person.name
- 获取构造方法们
- Constructor<?>[] getConstructors()
- Constructor getConstructor(类<?>… parameterTypes)
- Constructor getDeclaredConstructors(类<?>… parameterTypes)
- Constructor<?>[] getDeclaredConstructors()
- 获取成员方法们
- Method[] getMethods()
- Method getMethod(String name, 类<?> …parameterTypes)
- Method[] getDeclaredMethods()
- Method getDeclaredMethod(String name, 类<?> …parameterTypes)
- 获取类名
- String getName
Class<Person> cls = Person.class; Method eat = cls.getMethod("eat", String.class); Person p = new Person(); eat.invoke(p, "饭"); // 吃饭了 Method[] methods = cls.getMethods(); for (Method method : methods) { System.out.println(method); method.setAccessible(true); // 也支持暴力反射 System.out.println(method.getName()); // 获取方法名称 }
- 获取成员变量们
-
案例
- 需求:写一个框架,不能改变任何代码的前提下,可以帮我们创建任意类的对象,并且执行其中任意方法
- 配置文件
- 反射
// 配置 pro.properties 文件(只需要改动配置文件即可) className=com.bright.domain.Student methodName=sleep // 可以执行任意类和方法 // 加载配置文件 Properties pro = new Properties(); ClassLoader classLoader = ReflectTest.class.getClassLoader(); InputStream is = classLoader.getResourceAsStream("pro.properties"); pro.load(is); String className = pro.getProperty("className"); String methodName = pro.getProperty("methodName"); // 加载该类进内存 Class<?> cls = Class.forName(className); Object obj = cls.getDeclaredConstructor().newInstance(); Method method = cls.getMethod(methodName); method.invoke(obj);
- 需求:写一个框架,不能改变任何代码的前提下,可以帮我们创建任意类的对象,并且执行其中任意方法
-
I18Na案例
- 创建config.properties配置文件
language=com.bright.i18n.Zhcn
- 使用
// 创建I18N接口 public interface I18N { String say(); } // 实现类 public class Zhcn implements I18N { @Override public String say() { return "生命不息奋斗不止"; } } // 使用 @Test public static void say() { Properties prop = new Properties(); String configPath = Application.class.getResource("/config.properties").getPath(); try { configPath = URLDecoder.decode(configPath, "UTF-8"); // 加载配置文件 prop.load(new FileInputStream(configPath)); String language = prop.getProperty("language"); I18N i18n = (I18N)Class.forName(language).getDeclaredConstructor().newInstance(); System.out.println(i18n.say()); } catch (Exception e) { e.printStackTrace(); } }
2.15. 注解
-
概念:说明程序的,给计算机看的
-
作用分类
- 编写文档:通过代码里标识的注解生成文档【生成文档 doc 文档】
- 代码分析:通过代码里标识的注解对代码进行分析【使用反射】
- 编译检查:通过代码里标识的注解让编译器能够实现基本的编译检查【Override】
-
JDK 内置注解
- @Override:检测被该注解标注的方法是否是继承自父类(接口)的
- @Deprecated:该注解标注的内容表示已过时
- @SuppressWarnings:压制警告
- 一般传递参数 all
-
自定义注解
-
格式:元注解 @interface 注解名称()
-
本质:注解就是一个接口,该接口默认继承 Annotation 接口
-
属性:接口中可以定义的成员方法(抽象方法)
-
要求:属性的返回值类型:基本数据类型,String,枚举,注解,以上类型的数组(void 和自定义类不行)
-
定义了属性,在使用时需要给属性赋值
- 如果定义属性时使用 default 关键字给属性默认初始化值,则使用注解时,可以不进行属性的赋值
- 如果只有一个属性需要赋值并属性的名称是 value 直接定义就 ok
- 数组赋值时,值用 {} 包裹,如果数组只有一个值则 {} 可省略
@SuppressWarnings("all") public @interface MyAnno { int value(); String name() default "JavaScript"; // 可以不赋值取默认值 String[] strs(); } @MyAnno(value = 15, strs = {"Java", "JavaScript"}) public class Worker { }
-
-
元注解:用于描述注解的注解
- @Target:描述注解能够作用的位置
- ElementType取值
- TYPE:可以作用于类上
- METHOD:可以作用于方法上
- FIELD:可以作用于成员变量上
- ElementType取值
- @Retention:描述注解被保留的阶段
- @Retention(RetentionPolicy.RUNTIME):当前被描述的注解会保留到 class 字节码文件中,被 JVM 读取到
- @Documented:描述注解是否被抽取到 api 文档中
- @Inherited:描述注解是否被子类继承
@Target(value = {ElementType.TYPE, ElementType.METHOD, ElementType.FIELD}) // 表示该注解只能作用于类上 @Documented @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface MyAnno1 { }
- @Target:描述注解能够作用的位置
-
解析注解
@Target(ElementType.TYPE) // 作用在类上 @Retention(RetentionPolicy.RUNTIME) // 作用在运行期阶段 public @interface Pro { String className(); String methodName(); } // 测试的类(注解中传递的全限定类名跟方法 public class Test01 { public void show() { System.out.println("Test01...show..."); } } // 注解解析并执行方法 // 解析注解 Class<ReflectTest> cls1 = ReflectTest.class; // 获取注解对象 Pro an = cls1.getAnnotation(Pro.class); // 在内存中生成了一个该注解接口的子类实现对象 // 调用注解对象中定义的抽象方法, 获取返回值 String className = an.className(); String methodName = an.methodName(); System.out.println(className + "." + methodName); // com.bright.annotation.show // 加载类进内存 Class<?> cls2 = Class.forName(className); // 创建对象 Object obj = cls2.getDeclaredConstructor().newInstance(); // 获取方法对象 Method method = cls2.getMethod(methodName); method.invoke(obj);
-
注解_简单的测试框架
// 检测要出错的类 public class Calculator { // 加法 @Check public void add() { System.out.println("1 + 0" + (1 + 0)); } // 减法 @Check public void sub() { int a = 5 / 0; System.out.println("1 - 0" + (1 - 0)); } // 乘法 @Check public void mul() { String str = null; System.out.println(str.equals("1")); System.out.println("1 * 0" + (1 * 0)); } // 除法 @Check public void div() { System.out.println("1 / 0" + (1 / 0)); } @Check public void show() { System.out.println("永无bug..."); } } @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Check { } // 测试框架 // 创建计算器对象 Calculator calc = new Calculator(); // 获取字节码文件对象 Class<? extends Calculator> cls = calc.getClass(); // 获取计算器中所有的方法 Method[] methods = cls.getMethods(); int num = 0; // 出现异常出现的次数 BufferedWriter bw = new BufferedWriter(new FileWriter(".\\bug.txt")); for (Method method : methods) { if(method.isAnnotationPresent(Check.class)) { // 判断每一个方法是否有 Check 注解修饰 try { method.invoke(calc); } catch (Exception e) { // 铺货异常, 记录到文件中 num++; bw.write(method.getName() + "方法出异常了..."); bw.newLine(); bw.write("异常的名称:" + e.getCause().getClass().getSimpleName()); bw.newLine(); bw.write("异常的原因:" + e.getCause().getMessage()); bw.newLine(); bw.write("---------------------------"); bw.newLine(); } } } bw.write("本次测试一共出现" + num + "次异常"); bw.flush(); bw.close();
3.1. 设计模式
3.1.1. 单例模式
-
优点
- 在空间中只有一个对象,节省内存空间
- 避免频繁的创建对象销毁对象,提高性能
- 避免对共享资源的多重占用
-
缺点
- 扩展比较困难
- 如果实例化后的对象长期不使用,系统将默认认为垃圾进行回收,造成对象状态丢失
-
使用场景
- 创建对象时占用资源过多,但同时又需要用到该类的对象
- 对系统内资源要求统一读写,入读写配置信息
- 当多个实例存在可能引起程序逻辑错误,如号码生成器
-
饿汉式的代码实现(在类加载时就创建实例)
- 在多线程中是安全的
// 饿汉式:创建对象实例的时候直接初始化 public class SingletonOne { // 创建类中私有构造 private SingletonOne(){} // 创建该类型的私有静态实例 private static SingletonOne instance = new SingletonOne(); // 创建公有静态方法返回静态实例对象 public static SingletonOne getInstance(){ return instance; } }
-
测试
SingletonOne one = SingletonOne.getInstance(); SingletonOne two = SingletonOne.getInstance(); System.out.println(one == two); // true
-
懒汉式的代码实现(以时间换空间,第一次使用时进行初始化)
- 在多线程中存在线程的风险
- 解决方案:同步锁、双重校验锁、静态内部类、枚举
// 懒汉式:类中实例对象创建时并不直接初始化,直到第一次调用 getInstance 时才完成初始化 public class SingletonTwo { // 创建私有构造方法 private SingletonTwo(){} // 创建静态的该类实例对象 private static SingletonTwo instance = null; // 创建公有静态的该类实例对象 public static SingletonTwo getInstance() { if(instance == null) { instance = new SingletonTwo(); } return instance; } }
- 在多线程中存在线程的风险
-
测试
SingletonTwo one = SingletonTwo.getInstance(); SingletonTwo two = SingletonTwo.getInstance(); System.out.println(one == two); // true
3.1.2. 工厂模式
-
介绍
- 工厂模式用于隐藏创建对象的细节
- 工厂模式核心:工厂类:Factory
- 工厂模式可细分为简单工厂、工厂方法与抽象工厂
-
i18n国际化的简单应用
- 先创建factory包,包里创建I18N接口和其实现类,I18NFactory工厂类
- 在外界使用工厂类来创建实现类
// 接口 public interface I18N { String getTitle(); } // 工厂类 public class I18NFactory { // 一般为静态方法, 返回指定的实现类 public static I18N getI18NOnject(String area){ if(area.equals("china")) { return new Chinese(); } else if(area.equals("spain")) { return new Spainish(); } else if(area.equals("italy")) { return new Italian(); } else { return null; } } } // 实现类等 public class Chinese implements I18N { public String getTitle() { return "人事管理系统"; } } // 使用工厂来创建实现类 I18N i18N = new I18NFactory().getI18NOnject("italy"); System.out.println(i18N.getTitle());
-
使用工厂模式模拟多端应用切换
- 首先创建包device,包里创建Device接口、DesktopDevice与MobileDevice实现Device接口,最后创建工厂类根据判断返回相应的实现类
// Device接口 public interface Device { String getIndex(); String getDescription(); } // 两个实现类 public class DesktopDevice implements Device { public String getIndex() { return "/desktop/index.html"; } public String getDescription() { return "/desktop/desc.html"; } } public class MobileDevice implements Device { public String getIndex() { return "/mobile/index.html"; } public String getDescription() { return "/mobile/desc.html"; } } // 工厂类 public class DeviceFactory { public static Device getDevice(HttpServletRequest request) { String userAgent = request.getHeader("user-agent"); System.out.println(userAgent); if(userAgent.indexOf("Windows NT") != -1) { return new DesktopDevice(); } else if(userAgent.indexOf("iPhone") != -1 || userAgent.indexOf("Android") != -1) { return new MobileDevice(); } else { return null; } } } // 使用 Device de = DeviceFactory.getDevice(request); request.getRequestDispatcher(de.getIndex()).forward(request, response);