Java基础知识总结

Java基础知识总结

    • 1、数组
      • 1.1 Arrays.split(“”) 方法输入参数是正则串
    • 2、正则表达式的保留字符
    • 3、多态的理解
    • 4、对象的类型检查
    • 5、final修饰
    • 6、内部类的创建实例
    • 7、static修饰
      • 7.1 静态代码块
      • 7.2 静态属性的持久性
    • 8、枚举
    • 9、接口
      • 9.1接口的成员变量
    • 10、集合
      • 10.1 Set集合
      • 10.2 Map集合
    • 11、泛型
      • 11.1 Java8新增的几种泛型接口
        • 11.1.1 断言接口Predicate
        • 11.1.2 消费接口Consumer
        • 11.1.3 函数接口Function
    • 12、双冒号标记的方法引用
    • 13、字符串String
      • 13.1 调用String类的format方法进行字符串格式化之时,每种格式定义与数据类型是一一对应的
      • 13.2 把字符数组转成字符串
    • 14、使用option解决空指针异常问题
    • 15、反射
      • 15.1 通过反射来修改某个实例的私有属性
        • 注意:获取对象的所有属性使用getDeclaredFields(如果使用getFields,就无法获取到private的属性)
      • 15.2 利用反射技术操作私有方法
    • 16、注解
      • 16.1 系统自带注解
      • 16.2 注解的基本单元——元注解
      • 16.3 利用注解技术检查空指针
        • 1. 自定义新的非空注解
        • 2. 给非空字段添加非空注解
        • 3. 利用反射机制校验被非空注解修饰了的所有字段
        • 4. 在业务需要的地方调用校验方法
    • 17、文件读写
      • 17.1 文件与目录的管理
        • 1、检查文件状态
        • 2、获取文件信息
        • 3、管理文件操作
        • 4、遍历某目录下的文件
      • 17.2 通过字符流读写文件
      • 17.3 通过缓冲区读写文件
      • 17.4 采用缓存读取器和缓存写入器逐行复制
      • 17.5 RandomAccessFile 随机访问文件的读写
    • 18、I/O流
      • 18.1 文件字节I/O流
      • 18.2 缓存字节I/O流
      • 18.3 通过缓存输入和输出流复制文件
      • 18.4 序列化
      • 18.5 IO流处理简单的数据压缩
    • 19、NIO——非阻塞的IO
      • 19.1 文件通道的基本用法
      • 19.2 深入理解字节缓存ByteBuffer
      • 19.3 文件通道的性能优势
      • 19.4 NIO配套的文件工具Files
    • 20、线程
      • 20.1 线程基本用法
      • 20.2 利用Runnable启动线程
      • 20.3 利用Callable启动线程
      • 20.4 定时器与定时任务
    • 21、并发
      • 21.1 线程同步synchronized
      • 21.2 通过加解锁避免资源冲突
      • 21.3 线程间的通信方式
    • 22、线程池
      • 22.1 普通线程池的运用
      • 22.2 几种定时器线程池
      • 22.2 Fork+Join框架实现分而治之
    • 23、数据格式
      • 23.1 URL地址的组成格式
        • URL工具常用的方法:
        • 域名的合法性校验:
        • 常见字符对应的URL转义符:
      • 23.2 JSON串的定义和解析
      • 23.3 XML报文的定义和解析
    • 24、HTTP访问
      • 24.1 GET方式的HTTP调用
        • 网络传输中把输入流中的数据转换为字符串:
        • HTTP接口调用
      • 24.2 POST方式的HTTP调用
      • 24.3 Java11新增的HttpClient
      • 24.4 HttpClient实现下载与上传
    • 25、Socket通信
      • 25.1 利用Socket传输文本消息
      • 25.2 使用Socket开展文件传输
      • 25.3 采用UDP协议的Socket通信
    • 26、JDBC编程
      • 26.1 JDBC的应用原理
      • 26.2 通过JDBC管理数据库
      • 26.3 引入预报告,防止SQL注入
    • ———————————————————————————
    • JVM相关
      • 1、eclipse开发java内存设置

原笔记网址

1、数组

1.1 Arrays.split(“”) 方法输入参数是正则串

split方法的输入参数理应是个正则串,并非普通的分隔字符。由于点号和竖线都是正则串的保留字符,因此无法直接在正则串中填写,必须进行转义处理方可。 正则字符串通过在原字符前面添加两个反斜杆来转义,像点号字符在正则串中对应的转义符为“.”,而竖线在正则串中对应的转义符为“|”。

// 通过点号分割字符串
private static void splitByDot() {    
	String dotStr = "123.456.789";    
	// split(".")无法得到分割后的字符串数组    
	//String[] dotArray = dotStr.split(".");    
	// 点号是正则串的保留字符,需要进行转义(在点号前面加两个反斜杆)
	String[] dotArray = dotStr.split("\\.");    
	for (String item : dotArray) { 
		System.out.println("dot item = "+item);    
	}
}

// 通过竖线分割字符串
private static void splitByLine() {    
	String lineStr = "123|456|789";    
	// split("|")分割得到的字符串数组,每个数组元素只有一个字符,类似于toCharArray的结果    
	//String[] lineArray = lineStr.split("|");    
	// 竖线是正则串的保留字符,需要进行转义(在竖线前面加两个反斜杆)    
	String[] lineArray = lineStr.split("\\|");    
	for (String item : lineArray) {       
		System.out.println("line item = "+item);    
	}
}

2、正则表达式的保留字符

保留字符 说明
() 把圆括号内外的表达式区别开来。
[] 表示方括号内部的字符互相之间是或的关系。
{} 花括号中间填写数字,表示花括号前面的字符有多少位。
| 对前面和后面的字符进行或运算,表示既可以是前面的字符,也可以是后面的字符。
- 与前面和后面的字符组合起来,代表两个字符之间的所有连续字符。
. 代表除了回车符和换行符以外的其它字符。
+ 表示加号前面的字符可以有一位,也可以有多位。
* 表示星号前面的字符可以有一位,也可以有多位,还可以没有(0位)。
\ 两个反斜杆可对保留字符进行转义,表示保留字符的自身符号。

3、多态的理解

公鸡Cock 和母鸡Hen都继承自Chicken

//演示类的多态性
public class TestChicken {    
	public static void main(String[] args) {        
		// 鸡类的实例变成了一只公鸡        
		Chicken chicken = new Cock();        
		// 此时鸡类的叫声就变为公鸡的叫声        
		chicken.call();        
		// 鸡类的实例变成了一只母鸡       
		chicken = new Hen();        
		// 此时鸡类的叫声就变为母鸡的叫声        
		chicken.call();    
	}
}

//喔喔喔咯咯咯

引入多态概念的好处是:
只要某些类型都从同一个父类派生而来,就能在方法内部把它们当作同一种类型来处理,而无需区分具体的类型。仍以鸡叫为例,不管是公鸡叫还是母鸡叫,都是某种鸡在叫,于是完全可以定义一个叫唤方法,根据输入的鸡参数,让这只鸡自己去叫即可。叫唤方法的具体代码如下所示:

// 定义一个叫唤方法,传入什么鸡,就让什么鸡叫 
private static void call(Chicken chicken) { 
    chicken.call(); 
}

这下有了通用的鸡叫方法,外部就能把鸡类的实例作为输入参数填进去。当输入参数为公鸡实例的时候,call方法上演的是公鸡喔喔叫;当输入参数为母鸡实例的时候,call方法上演的是母鸡咯咯叫。

call(new Cock()); // 公鸡叫 
call(new Hen()); // 母鸡叫

4、对象的类型检查

“A instanceof B”,意思是检查A实例是否属于B类型

// 利用关键字instanceof检查某实例的归属类
private static void checkInstance(Chicken chicken) {
    if (chicken instanceof Cock) { // 判断这只鸡是不是公鸡
        System.out.println("检查对象实例:这是只公鸡。");
    } else if (chicken instanceof Hen) { // 判断这只鸡是不是母鸡
        System.out.println("检查对象实例:这是只母鸡。");
    } else {
        System.out.println("检查对象实例:这既不是公鸡也不是母鸡。");
    }
}

5、final修饰

一旦某个类被final修饰,则该类无法再派生出任何子类。	
一旦某个成员属性被final修饰,则该属性不能再次赋值了。
一旦某个成员方法被final修饰,则该方法禁止被子类重载。

6、内部类的创建实例

内部类:

//演示内部类的简单定义
public class Tree {
    private String tree_name;
    
    public Tree(String tree_name) {
        this.tree_name = tree_name;
    }
    public void sprout() {
        System.out.println(tree_name+"发芽啦");
        // 外部类访问它的内部类,就像访问其它类一样,都要先创建类的实例,再访问它的成员
        Flower flower = new Flower("花朵");
        flower.bloom();
    }
    
    // Flower类位于Tree类的内部,它是个内部类
    public class Flower {
        private String flower_name;
        public Flower(String flower_name) {
            this.flower_name = flower_name;
        }
        public void bloom() {
            System.out.println(flower_name+"开花啦");
        }
    }
}

直接通过new关键字是无法创建内部类实例的。只有先创建外部类的实例,才能基于该实例去new内部类的实例,内部实例的创建代码格式形如“外部类的实例名.new 内部类的名称(…)

// 先创建外部类的实例,再基于该实例去创建内部类的实例
TreeInner inner = new TreeInner("桃树");
// 创建一个内部类的实例,需要在new之前添加“外层类的实例名.”
TreeInner.Flower flower = inner.new Flower("桃花");
flower.bloom(); // 调用内部类实例的bloom方法

当内部类与外部类有重名属性时:

// 该方法访问内部类自身的tree_name字段
public void bloomInnerTree() {
    // 内部类里面的this关键字指代内部类自身
    System.out.println(this.tree_name+"的"+flower_name+"开花啦");
}


// 该方法访问外部类TreeInner的tree_name字段
public void bloomOuterTree() {
    // 要想在内部类里面访问外部类的成员,就必须在this之前添加“外部类的名称.”
    System.out.println(TreeInner.this.tree_name+"的"+flower_name+"开花啦");
}

内部静态类:

//演示嵌套类的定义
public class TreeNest {
    private String tree_name;
    public TreeNest(String tree_name) {
        this.tree_name = tree_name;
    }
    public void sprout() {
        System.out.println(tree_name+"发芽啦");
    }
    
    // Flower类虽然位于TreeNest类的里面,但是它被static修饰,故而与TreeNest类的关系比起一般的内部类要弱。
    // 为了与一般的内部类区别开来,这里的Flower类被叫做嵌套类。
    public static class Flower {
        private String flower_name;
        
        public Flower(String flower_name) {
            this.flower_name = flower_name;
        }
        public void bloom() {
            System.out.println(flower_name+"开花啦");
        }
        public void bloomOuterTree() {
            // 注意下面的写法是错误的,嵌套类不能直接访问外层类的成员
            //System.out.println(TreeNest.this.tree_name+"的"+flower_name+"开花啦");
        }
    }
}

// 演示嵌套类的调用方法
private static void testNest() {
    // 创建一个嵌套类的实例,格式为“new 外层类的名称.嵌套类的名称(...)”
    TreeNest.Flower flower = new TreeNest.Flower("桃花");
    flower.bloom();
}

7、static修饰

7.1 静态代码块

静态代码块作用是在系统加载该类之时立即执行这部分代码,静态代码块与构造方法比起来,它们的执行顺序?
静态代码块先于构造方法

// 叶子数量,用来演示构造方法与初始静态代码块的执行顺序
public static int leaf_count = 0;

// static还能用来包裹某个代码块,一旦当前类加载进内存,静态代码块就立即执行
static {
	leaf_count++;
	System.out.println("这里是初始的静态代码块,此时叶子数量为"+leaf_count);
}

public TreeStatic(String tree_name) {
	this.tree_name = tree_name;
	leaf_count++;
	System.out.println("这里是构造方法,此时叶子数量为"+leaf_count);
}
// 演示静态代码块与构造方法的执行顺序
private static void testStaticBlock() {
	System.out.println("开始创建树木类的实例");
	TreeStatic tree = new TreeStatic("月桂");
	System.out.println("结束创建树木类的实例");
}
这里是初始的静态代码块,此时叶子数量为1
开始创建树木类的实例
这里是构造方法,此时叶子数量为2
结束创建树木类的实例

7.2 静态属性的持久性

凡是被static修饰的静态变量,它在内存中占据了一块固定的区域,不管所在类被创建了多少个实例,每个实例引用的静态变量依然是最初分配的那个。

// 树木年轮,用来演示静态属性的持久性
public static int annual_ring = 0;

// 注意每次读取静态属性,得到的都是该属性最近一次的数值
public void grow() {
	annual_ring++;
	System.out.println(tree_name+"的树龄为"+annual_ring);
}
// 演示静态属性的持久性
private static void testStaticProperty() {
	TreeStatic bigTree = new TreeStatic("大树");
	bigTree.grow();
	TreeStatic littleTree = new TreeStatic("小树");
	littleTree.grow();
}
这里是构造方法,此时叶子数量为3
大树的树龄为1
这里是构造方法,此时叶子数量为4
小树的树龄为2

8、枚举

枚举类型提供的通用方法主要有两个,其中ordinal方法可获得该枚举的序号,toString可获得该枚举的字段名称

// 演示简单枚举类型的调用方式
private static void testEnum() {
	Season spring = Season.SPRING; // 声明一个春天的季节实例
	Season summer = Season.SUMMER; // 声明一个夏天的季节实例
	Season autumn = Season.AUTUMN; // 声明一个秋天的季节实例
	Season winter = Season.WINTER; // 声明一个冬天的季节实例
	// 枚举类型提供的通用方法主要有两个,
	// 其中ordinal方法可获得该枚举的序号,toString可获得该枚举的字段名称
	System.out.println("spring number="+spring.ordinal()+", name="+spring.toString());
	System.out.println("summer number="+summer.ordinal()+", name="+summer.toString());
	System.out.println("autumn number="+autumn.ordinal()+", name="+autumn.toString());
	System.out.println("winter number="+winter.ordinal()+", name="+winter.toString());
}
spring number=0, name=SPRING
summer number=1, name=SUMMER
autumn number=2, name=AUTUMN
winter number=3, name=WINTER

自定义枚举

//演示枚举类型的扩展定义
public enum SeasonCn {
	// 在定义枚举变量的同时,调用该枚举变量的构造方法
	SPRING(1,"春天"), SUMMER(2,"夏天"), AUTUMN(3,"秋天"), WINTER(4,"冬天");

	private int value; // 定义季节的阿拉伯数字
	private String name; // 定义季节的中文名称字段

	// 在构造方法中传入该季节的阿拉伯数字和中文名称
	private SeasonCn(int value, String name) {
		this.value = value;
		this.name = name;
	}

	// 获取季节的阿拉伯数字
	public int getValue() {
		return this.value;
	}

	// 获取季节的中文名称
	public String getName() {
		return this.name;
	}
}

需将原来的ordinal方法替换为getValue方法,将原来的toString方法替换为getName方法。

// 演示扩展枚举类型的调用方式
private static void testEnumCn() {
	SeasonCn spring = SeasonCn.SPRING; // 声明一个春天的季节实例
	SeasonCn summer = SeasonCn.SUMMER; // 声明一个夏天的季节实例
	SeasonCn autumn = SeasonCn.AUTUMN; // 声明一个秋天的季节实例
	SeasonCn winter = SeasonCn.WINTER; // 声明一个冬天的季节实例
	// 通过扩展而来的getName方法,可获得该枚举预先设定的中文名称
	System.out.println("spring number="+spring.getValue()+", name="+spring.getName());
	System.out.println("summer number="+summer.getValue()+", name="+summer.getName());
	System.out.println("autumn number="+autumn.getValue()+", name="+autumn.getName());
	System.out.println("winter number="+winter.getValue()+", name="+winter.getName());
}

spring number=1, name=春天
summer number=2, name=夏天
autumn number=3, name=秋天
winter number=4, name=冬天

9、接口

9.1接口的成员变量

(java8以前)
接口内部的属性,则默认为终态属性,即添加了final前缀的成员属性。

(java8及以后)
1、增加了默认方法,并通过前缀default来标识。接口内部需要编写默认方法的完整实现代码,这样实现类无需重写该方法即可直接继承并使用,仿佛默认方法就是父类方法一样,唯一的区别在于实现类不允许重写默认方法
2、增加了静态属性和静态方法,而且都通过前缀static来标识。接口的静态属性同时也是终态属性,初始化赋值之后便无法再次修改;接口的静态方法不能被实现类继承,因而实现类允许定义同名的静态方法,缘于接口的静态方法与实现类的静态方法没有任何关联,仅仅是它俩恰好同名而已。

//定义一个增加了Java8新特性的接口
public interface ExpandBehavior {

	// 声明了一个抽象的飞翔方法
	public void fly();

	// 声明了一个抽象的游泳方法
	public void swim();

	// 声明了一个抽象的奔跑方法
	public void run();
	
	// 默认方法,以前缀default标识。默认方法不支持重写,但可以被继承。
	public default String getOrigin(String place, String name, String ancestor) {
		return String.format("%s%s的祖先是%s。", place, name, ancestor);
	}

	public static int MALE = 0;
	public static int FEMALE = 1;
	// 接口内部的静态属性也默认为终态属性,所以final前缀可加可不加
	//public final static int MALE = 0;
	//public final static int FEMALE = 1;
	// 静态方法,以关键字static标识。静态方法支持重写,但不能被继承。
	public static String getNameByLeg(int leg_count) {
		if (leg_count == 2) {
			return "二足动物";
		} else if (leg_count == 4) {
			return "四足动物";
		} else if (leg_count >= 6) {
			return "多足动物";
		} else {
			return "奇异动物";
		}
	}
}

根据上面的扩展接口,重新编写实现了该接口的鹅类,其中fly、swim、run这三个抽象方法均须重写,唯有默认方法getOrigin不要重写,并且鹅类代码当中可以直接调用这个默认方法。新写的鹅类代码ExpandGoose示例如下:

//定义实现了扩展接口的鹅类
public class ExpandGoose extends Bird implements ExpandBehavior {

	public ExpandGoose(String name, int sexType) {
		super(name, sexType);
	}

	// 实现了接口的fly方法
	@Override
	public void fly() {
		System.out.println("鹅飞不高,也飞不远。");
	}

	// 实现了接口的swim方法
	@Override
	public void swim() {
		System.out.println("鹅,鹅,鹅,曲项向天歌。白毛浮绿水,红掌拨清波。");
	}

	// 实现了接口的run方法
	@Override
	public void run() {
		System.out.println("槛外萧声轻荡漾,沙间鹅步满蹒跚。");
	}
	
	// 根据产地和祖先拼接并打印该动物的描述文字
	public void show(String place, String ancestor) {
		// getOrigin是来自扩展接口ExpandBehavior的默认方法,可以在实现类中直接使用
		String desc = getOrigin(place, getName(), ancestor);
		System.out.println(desc);
	}
}

接着轮到外部访问这个鹅类ExpandGoose了,表面上外部仍跟平常一样调用鹅类的成员方法,然而在调用接口的静态成员时有所差别。对于接口的静态属性,外部依然能够通过鹅类直接访问,访问格式形如“实现类的名称.静态属性名”;对于接口的静态方法,外部却不能通过鹅类访问了,因为实现类并未继承接口的静态方法,所以外部只能通过接口自身访问它的静态方法,访问格式形如“扩展接口的名称.静态方法名(***)”。下面是外部调用鹅类ExpandGoose的代码例子:

// 演示扩展接口的实现类用法
private static void testExpand() {
	// 实现类可以继承接口的静态属性
	ExpandGoose goose = new ExpandGoose("鹅", ExpandGoose.FEMALE);
	goose.show("中国", "鸿雁");
	goose.show("欧洲", "灰雁");
	// 接口中的静态方法没有被实现类所继承,因而只能通过扩展接口自身访问
	String typeName = ExpandBehavior.getNameByLeg(2);
	System.out.println("鹅是"+typeName);
}
中国鹅的祖先是鸿雁。
欧洲鹅的祖先是灰雁。
鹅是二足动物

10、集合

10.1 Set集合

集合分成哈希集合(HashSet)与二叉集合(TreeSet)两类。

  • 哈希集合里面保存的各元素是无序的,因为一个数据的哈希结果是散列值。
  • 由于二叉集合内部采取二叉树存储数据,每次新增元素的操作都会进行大小比较,最终得到的二叉集合必定是有序的。

如果是开发者自己定义的数据类型(新的类),就要求开发者自己来实现计算方法和比较方法。譬如有个自定义的手机类MobilePhone,该类的定义代码见下:

//定义一个手机类
public class MobilePhone {
	private String brand; // 手机品牌
	private Integer price; // 手机价格

	public MobilePhone(String brand, int price) {
		this.brand = brand;
		this.price = price;
	}

	// 获取手机品牌
	public String getBrand() {
		return this.brand;
	}

	// 获取手机价格
	public int getPrice() {
		return this.price;
	}
}

就哈希集合的哈希值计算而言,自定义的手机类需要重写hashCode和equals方法,其中hashCode方法计算得到的哈希值对应于该对象的保存位置,而equals方法用来判断该位置上的几个元素是否完全相等。一方面,我们要保证品牌与价格都相同的两个元素,它们的哈希值必须也相等;另一方面,即使两个元素的品牌和价格不一致,它们的哈希值也可能恰巧相等,于是还需要equals方法进一步校验是否存在重复。按照上述要求,重写后的hashCode和equals方法代码示例如下:

// hashCode方法计算出来的哈希值对应于该对象的保存位置
@Override
public int hashCode() {
	return brand.hashCode() + price.hashCode();
}

// 同一个存储位置上可能有多个对象(哈希值恰好相等),
// 此时系统自动调用equals方法判断是否存在相同的对象。
@Override
public boolean equals(Object obj) {
	if (!(obj instanceof MobilePhoneHash)) {
		return false;
	}
	MobilePhoneHash other = (MobilePhoneHash) obj;
	// 手机品牌和手机价格都相等,才算是这两个手机相等
	boolean equals = this.brand.equals(other.brand) && this.price.equals(other.price);
	return equals;
}

至于二叉集合的节点大小比较,则手机类实现接口Comparable并具体定义该接口声明的compareTo方法(该方法用来比较两个元素的大小关系)。其实这里的Comparable接口与数组排序用到的Comparator接口作用类似,都是判断两个对象谁大谁小。如果要求二叉集合里面的手机元素按照价格排序,则compareTo方法主要校验当前手机的价格与其它手机的价格。详细的接口实现代码如下所示:

public class MobilePhoneTree implements Comparable<MobilePhoneTree> {
	// 此处省略手机类的构造方法、成员属性与成员方法定义

	// 二叉树除了检查是否相等,还要判断先后顺序。
	// 相等和先后顺序的校验结果从compareTo方法获得。
	@Override
	public int compareTo(MobilePhoneTree other) {
		if (this.price.compareTo(other.price) > 0) { // 当前价格较高
			return 1;
		} else if (this.price.compareTo(other.price) < 0) { // 当前价格较低
			return -1;
		} else {
			return this.brand.compareTo(other.brand);
		}
	}
}

结果应当为:哈希集合不会插入重复的手机对象,并且二叉集合里的各手机元素按照价格升序排列。

10.2 Map集合

遍历映射内部的所有元素

  • 1、通过迭代器遍历
    首先调用映射实例的 entrySet 获得该映射的集合入口,再调用入口对象的iterator方法获得映射的迭代器,然后使用迭代器遍历整个映射。
// 第一种遍历方式:显式指针,即使用迭代器
Set<Map.Entry<String, MobilePhone>> entry_set = map.entrySet();
Iterator<Map.Entry<String, MobilePhone>> iterator = entry_set.iterator();
while (iterator.hasNext()) {
	// 注意这里要先把入口取出来,这样才能分别getKey和getValue
	Map.Entry<String, MobilePhone> iterator_item = iterator.next();
	// 获取该键值对的键名
	String key = iterator_item.getKey();
	// 获取该键值对的键值
	MobilePhone value = iterator_item.getValue();
	System.out.println(String.format("iterator_item key=%s, value=%s %d", 
			key, value.getBrand(), value.getPrice()));
}

  • 2、通过for循环遍历
// 第二种遍历方式:隐式指针,即使用for循环
for (Map.Entry<String, MobilePhone> for_item : map.entrySet()) {
	// 获取该键值对的键名
	String key = for_item.getKey();
	// 获取该键值对的键值
	MobilePhone value = for_item.getValue();
	System.out.println(String.format("for_item key=%s, value=%s %d", 
			key, value.getBrand(), value.getPrice()));
}

  • 3、通过键名集合遍历
// 第三种遍历方式:先获得键名的集合,再通过键名集合遍历整个映射
// 注意:HashMap的keySet方法返回的是无序集合
Set<String> key_set = map.keySet();
for (String key : key_set) {
	// 通过键名获取该键值对的键值
	MobilePhone value = map.get(key);
	System.out.println(String.format("set_item key=%s, value=%s %d", 
			key, value.getBrand(), value.getPrice()));
}

  • 4、通过forEach方法遍历
// 第四种遍历方式:使用forEach方法夹带Lambda表达式进行遍历
map.forEach((key, value) -> 
		System.out.println(String.format("each_item key=%s, value=%s %d", 
				key, value.getBrand(), value.getPrice())) );

11、泛型

问号跟泛型比较的话,主要有以下几点区别:

  1. 问号只能用于给泛型类创建实例,本身不能创建实例。而泛型T既可用于泛型类创建实例,也可用于给自身创建实例,如“T t;”

  2. 问号只可用作输入参数,不可用作输出参数。而泛型T用于二者皆可。

  3. 使用了问号的容器实例,只允许调用get方法,不允许调用add方法。而泛型容器不存在方法调用的限制。

11.1 Java8新增的几种泛型接口

11.1.1 断言接口Predicate

先定义一个苹果类Apple

//定义一个苹果类
public class Apple {
	private String name; // 名称
	private String color; // 颜色
	private Double weight; // 重量
	private Double price; // 价格

	public Apple(String name, String color, Double weight, Double price) {
		this.name = name;
		this.color = color;
		this.weight = weight;
		this.price = price;
	}

	// 为节省篇幅,此处省略每个成员属性的get/set方法

	// 获取该苹果的详细描述文字
	public String toString() {
		return String.format("\n(name=%s,color=%s,weight=%f,price=%f)", name,
				color, weight, price);
	}

	// 判断是否红苹果
	public boolean isRedApple() {
		return this.color.toLowerCase().equals("red");
	}
}

接着构建一个填入若干苹果信息的初始清单

// 获取默认的苹果清单
private static List<Apple> getAppleList() {
	// 数组工具Arrays的asList方法可以把一系列元素直接赋值给清单对象
	List<Apple> appleList = Arrays.asList(
					new Apple("红苹果", "RED", 150d, 10d), 
					new Apple("大苹果", "green", 250d, 10d),
					new Apple("红苹果", "red", 300d, 10d), 
					new Apple("大苹果", "yellow", 200d, 10d), 
					new Apple("红苹果", "green", 100d, 10d), 
					new Apple("大苹果", "Red", 250d, 10d));
	return appleList;
}
public interface Predicate<T> {
    boolean test(T t);
}
// 利用系统自带的断言接口Predicate,对某个清单里的元素进行过滤
private static <T> List<T> filterByPredicate(List<T> list, Predicate<T> p) {
	List<T> result = new ArrayList<T>();
	for (T t : list) {
		if (p.test(t)) { // 如果满足断言的测试条件,则把该元素添加到新的清单
			result.add(t);
		}
	}
	return result;
}

终于轮到外部调用刚才的过滤方法了,现在要求从原始的苹果清单中挑出所有的红苹果

// 测试系统自带的断言接口Predicate
private static void testPredicate() {
	List<Apple> appleList = getAppleList();
	// 第一种调用方式:匿名内部类实现Predicate。挑出所有的红苹果
	List<Apple> redAppleList = filterByPredicate(appleList, new Predicate<Apple>() {
		@Override
		public boolean test(Apple t) {
			return t.isRedApple();
		}
	});
	System.out.println("红苹果清单:" + redAppleList.toString());
}

显然匿名内部类的实现代码过于冗长,改写为Lambda表达式的话仅有以下一行代码:

// 第二种调用方式:Lambda表达式实现Predicate
List<Apple> redAppleList = filterByPredicate(appleList, t -> t.isRedApple());

或者采取方法引用的形式,也只需下列的一行代码:

// 第三种调用方式:通过方法引用实现Predicate
List<Apple> redAppleList = filterByPredicate(appleList, Apple::isRedApple);

11.1.2 消费接口Consumer

断言接口只进行逻辑判断,不涉及到数据修改,若要修改清单里的元素,就用到了另一个消费接口Consumer。

public interface Consumer<T> {
    void accept(T t);
}

定义泛型方法modifyByConsumer,根据输入的清单数据和消费实例,从而对清单执行指定的消费行为。

// 联合运用Predicate和Consumer,可筛选出某些元素并给它们整容
private static <T> void selectAndModify(List<T> list, Predicate<T> p, Consumer<T> c) {
	for (T t : list) {
		if (p.test(t)) { // 如果满足断言的条件要求,
			c.accept(t); // 就把该元素送去美容院整容。
		}
	}
}
// 联合测试断言接口Predicate和消费接口Consumer
private static void testPredicateAndConsumer() {
	List<Apple> appleList = getAppleList();
	// 如果是红苹果,就涨价五成
	selectAndModify(appleList, t -> t.isRedApple(), t -> t.setPrice(t.getPrice() * 1.5));
	// 如果重量大于半斤,再涨价五成
	selectAndModify(appleList, t -> t.getWeight() >= 250, t -> t.setPrice(t.getPrice() * 1.5));
	System.out.println("涨价后的苹果清单:" + appleList.toString());
}

11.1.3 函数接口Function

Function的定义代码可知,该接口不但支持输入某个泛型变量,也支持返回另一个泛型变量。

public interface Function<T, R> {
    R apply(T t);
}

编写新的泛型方法recycleByFunction,该方法输入原始清单和函数实例,输出处理后的新清单,从而满足了数据抽取的功能需求。详细的方法代码示例如下:

// 利用系统自带的函数接口Function,把所有元素进行处理后加到新的清单里面
private static <T, R> List<R> recycleByFunction(List<T> list, Function<T, R> f) {
	List<R> result = new ArrayList<R>();
	for (T t : list) {
		R r = f.apply(t); // 把原始材料t加工一番后输出成品r
		result.add(r); // 把成品r添加到新的清单
	}
	return result;
}

12、双冒号标记的方法引用

参考Java开发笔记(六十三)双冒号标记的方法引用

表达式“(str) -> str.isEmpty()”满足了下列三个条件:

  1. 里面的str为字符串String类型,并且式子右边调用的isEmpty正好属于字符串变量的方法;
  2. 式子左边有且仅有一个String类型的参数,同时式子右边有且仅有一行字符串变量的方法调用;
  3. isEmpty的返回值为boolean布尔类型,Lambda表达式对应的匿名方法的返回值也是布尔类型;
String::isEmpty

String:参数类型
该表达式返回值类型:isEmpty的结果

若表达式需要传入参数:

String::endsWith, "y"

方法引用的条件非常严格,符合条件的表达式只能有方法自身,不允许出现其它额外的逻辑运算。被引用方法的输入参数尚能通过给过滤器添加参数来实现,多出来的逻辑运算可就无能为力了。不过对于字符串的筛选过程来说,更复杂的条件判断完全能够交给正则匹配方法matches,只要给定待筛选的字符串格式规则,那么matches方法就可以自动校验某个字符串是否符合正则条件了。假如要挑选首字母为w或者W的字符串数组,则采取方法引用的matches调用代码如下所示:

// 如需对字符串进行更复杂的条件筛选,可利用matches方法通过正则表达式来校验
resultArray = StringUtil.select2(strArray, String::matches, "[wW][a-zA-Z]*");
print(resultArray, "matches方法");

13、字符串String

13.1 调用String类的format方法进行字符串格式化之时,每种格式定义与数据类型是一一对应的

格式 数据类型
%d 整型
%s 字符串
%b 布尔值

13.2 把字符数组转成字符串

char[] temp = new char['a','b'];
String content = new String(temp); // 把数组转为字符串

14、使用option解决空指针异常问题

Optional本质上是一种特殊的容器,其内部有且仅有一个元素,同时该元素还可能为空。围绕着这个可空元素,Optional衍生出若干泛型方法,目的是将复杂的流程控制语句归纳为接续进行的方法调用。为了兼容已有的Java代码,通常并不直接构造Optional实例,而是调用它的ofNullable方法塞入某个实体对象,再调用Optional实例的其它方法进行处理。Optional常用的实例方法罗列如下:

方法 说明
get 获取可选器中保存的元素。如果元素为空,则扔出无此元素异常NoSuchElementException。
isPresent 判断可选器中元素是否为空。非空返回true,为空返回false。
ifPresent 如果元素非空,则对该元素执行指定的Consumer消费事件。
filter 如果元素非空,则根据Predicate断言条件检查该元素是否符合要求,只有符合才原样返回,若不符合则返回空值。
map 如果元素非空,则执行Function函数实例规定的操作,并返回指定格式的数据。
orElse 如果元素非空就返回该元素,否则返回指定的对象值。
orElseThrow 如果元素非空就返回该元素,否则扔出指定的异常。

举例:

常规的写法

// 判断是否红苹果
public boolean isRedApple() {
	// 常规的写法,判断color字段是否为空,再做分支处理
	boolean isRed = (this.color==null) ? false : this.color.toLowerCase().equals("red");
	return isRed;
}

Optional校验方式

// 把清单的非空判断代码改写为Optional校验方式
private static void getRedAppleWithOptionalTwo(List<Apple> list) {
	List<Apple> redAppleList = new ArrayList<Apple>();
	Optional.ofNullable(list) // 构造一个可空对象
		.ifPresent( // ifPresent指定了list非空时候的处理
			apples -> {
				apples.stream().forEach( // 对苹果清单进行流式处理
						item -> {
							if (Optional.ofNullable(item) // 构造一个可空对象
									.map(apple -> apple.isRedApple()) // map指定了item非空时候的取值
									.orElse(false)) { // orElse指定了item为空时候的取值
								redAppleList.add(item);
							}
						});
			});
	System.out.println("Optional2判断 红苹果清单=" + redAppleList.toString());
}

进一步优化,引入流式处理的filter方法替换if语句。

// 联合运用Optional校验和流式处理
private static void getRedAppleWithOptionalThree(List<Apple> list) {
	List<Apple> redAppleList = new ArrayList<Apple>();
	Optional.ofNullable(list) // 构造一个可空对象
			.ifPresent(apples -> { // ifPresent指定了list非空时候的处理
				// 从原始清单中筛选出红苹果清单
					redAppleList.addAll(apples.stream()
							.filter(a -> a != null) // 只挑选非空元素
							.filter(Apple::isRedApple) // 只挑选红苹果
							.collect(Collectors.toList())); // 返回结果清单
				});
	System.out.println("Optional3判断 红苹果清单=" + redAppleList.toString());
}

15、反射

15.1 通过反射来修改某个实例的私有属性

参考笔记

从Field对象挖掘出sex属性的数值,还得继续下列两个步骤的处理:

  1. 调用Field对象的setAccessible方法,并传入true值,表示将该字段设置为允许访问,以解除private的限制
  2. 调用Field对象的getInt方法,并传入鸡类实例,表示准备从该示例中获取指定字段的整型值。同理调用getBoolean方法获取的是布尔值,调用getString方法获取的是字符串值。倘若是获取基本类型以外的类型值,则需先调用get方法获得Object对象,再强制转换为目标类型。
// 通过反射来修改某个实例的私有属性
private static void setReflectSex(Chicken chicken, int sex) {
	try {
		Class cls = Chicken.class; // 获得Chicken类的基因类型
		// 通过字段名称获取该类的字段对象
		Field sexField = cls.getDeclaredField("sex");
		if (sexField != null) {
			sexField.setAccessible(true); // 将该字段设置为允许访问
			sexField.setInt(chicken, sex); // 将某实例的该字段修改为指定数值
		}
	} catch (Exception e) { // 捕捉到了任何一种异常(错误除外)
		e.printStackTrace();
	}
}

注意:获取对象的所有属性使用getDeclaredFields(如果使用getFields,就无法获取到private的属性)

Field[] fields = cls.getDeclaredFields();

15.2 利用反射技术操作私有方法

个人理解: 相当于可以访问私有方法,但是不能篡改该方法
参考文章
参照私有属性的反射操作过程,私有方法的反射调用可分解为如下三个步骤:

  1. 调用Class对象的getDeclaredMethod方法,获取指定名称的方法对象,即Method对象;
  2. 调用Method对象的setAccessible方法,并传入true值,表示将该方法设置为允许访问,以解除private的限制
  3. 调用Method对象的invoke方法,并传入鸡类实例,酌情填写输入参数;

示例:

// 通过反射来调用某个实例的私有方法(getSex方法)
private static int getReflectSex(Chicken chicken) {
	int sex = -1;
	try {
		Class cls = Chicken.class; // 获得Chicken类的基因类型
		// 通过方法名称及参数列表获取该方法的Method对象
		Method method = cls.getDeclaredMethod("getSex");
		method.setAccessible(true); // 将该方法设置为允许访问
		sex = (int) method.invoke(chicken); // 调用某实例的方法并获得输出参数
	} catch (Exception e) { // 捕捉到了任何一种异常(错误除外)
		e.printStackTrace();
	}
	return sex;
}

// 通过反射来调用某个实例的私有方法(setSex方法)
private static void setReflectSex(Chicken chicken, int sex) {
	try {
		Class cls = Chicken.class; // 获得Chicken类的基因类型
		// 通过方法名称及参数列表获取该方法的Method对象
		// 之所以需要参数类型列表,是因为同名方法可能会被多次重载,重载后的方法通过参数个数与参数类型加以区分
		Method method = cls.getDeclaredMethod("setSex", int.class);
		method.setAccessible(true); // 将该方法设置为允许访问
		method.invoke(chicken, sex); // 携带输入参数调用某实例的方法
	} catch (Exception e) { // 捕捉到了任何一种异常(错误除外)
		e.printStackTrace();
	}
}

16、注解

16.1 系统自带注解

参考文章

注解 说明
@Override 方法重写
@FunctionalInterface 专门用来标记Java8规定的函数式接口。函数式接口是一类特殊的接口形式,它的内部有且仅有一个抽象方法,抽象方法多了不行,再来一个抽象方法的话,接口实例就没法简写为Lambda表达式,也就无法成为“函数式”接口。Java自带的几个函数式接口包括:比较器Comparator、断言接口Predicate、消费接口Consumer、函数接口Function、文件过滤器FileFilter、运行器Runnable等。
@Deprecated 过时的方法
@SupressWarnings 屏蔽警告
@SafeVarargs 兼容可变参数中的泛型参数,该注解告诉编译器:此处可变参数中的泛型是类型安全的,不必担心强制类型转换的问题。

16.2 注解的基本单元——元注解

参考文章

注解 说明
@Documented 它修饰的注解将被收录到Java的开发文档中,这意味着程序员编码时的快捷提示会出现已收入的注解。
@Target 它修饰的注解将作用于哪一类的代码实体,例如ElementType.METHOD规定@Override对方法有效
@Retention 它修饰的注解将被编译器保留至哪个阶段
@Inherited 它修饰的注解将允许被子类所继承。通常情况下,一个注解加在某个类上面的话,它只对当前类有效,而对当前类的子类无效。

Target 注解:

ElementType 说明
TYPE 类型,包括类、接口和枚举。
FIELD 字段,即类的属性。
METHOD 方法,但不包含构造方法。
PARAMETER 方法的参数。
CONSTRUCTOR 构造方法。
LOCAL_VARIABLE 局部变量。
ANNOTATION_TYPE 注解类型。
PACKAGE

Retention 注解:

Retention 说明
SOURCE 只在编码阶段保留
CLASS 保留在编译生成的class文件中,但不在运行时保留。这样从class文件反编译出来的源码仍可找到它所修饰的注解。
RUNTIME 一直保留至运行阶段。这样修饰后的注解可通过反射技术读取获得,以便代码在运行时动态校验注解。

16.3 利用注解技术检查空指针

个人总结概要:

 1. 此方法适用于判断某个类的实例的所有字段是否存在值为空的,这样如果某个地方需要用到该实例多个字段时只需要调用该方法即可,即使之后需要新增字段,只需要改动该类的定义即可,不需要改动判空处代码。不适合用来判断单独一个变量值为null

参考文章

具体的处理过程大致分为四个步骤:

1. 自定义新的非空注解

首先定义一个名叫“NotNull”的注解,并规定它用于在程序运行过程中检查字段是否为空。这里有两点值得特别关注:第一点,该注解的生效期间位于程序运行过程当中,意味着需要将它保留至运行阶段;第二点,该注解用于检查字段是否为空,意味着它的作用目标正好是字段。据此可编写如下所示的注解定义代码:

import java.lang.annotation.*;

@Documented // 该注解纳入到Java开发手册
@Target({ ElementType.FIELD }) // 该注解的作用目标是字段(属性)
@Retention(RetentionPolicy.RUNTIME) // 该注解保留至运行阶段,这样能够通过反射机制调用
//定义了一个注解,在interface前面加上符号“@”,表示这是个注解
public @interface NotNull {}

2. 给非空字段添加非空注解

接着修改苹果类的定义代码,在每个不能为空的字段上方添加注解“@NotNull”,表示这是个特殊字段,它必须有值而不允许是空指针,简而言之,该字段必须是非空字段。修改后的苹果类代码片段示例如下:

//定义一个苹果类
public class Apple {
	@NotNull // 通过注解声明该字段不可为空
	private String name; // 名称
	@NotNull // 通过注解声明该字段不可为空
	private String color; // 颜色
	@NotNull // 通过注解声明该字段不可为空
	private Double weight; // 重量
	@NotNull // 通过注解声明该字段不可为空
	private Double price; // 价格

	// 此处省略苹果类的剩余代码定义
}

3. 利用反射机制校验被非空注解修饰了的所有字段

在进行反射调用的时候,又可分为主要的三个步骤:

  1. 调用Class对象的getDeclaredFields方法,获得该类中声明的所有字段;
  2. 依次遍历这些字段,并调用字段对象的isAnnotationPresent方法,判断当前字段是否存在非空注解;
  3. 倘若存在非空注解,则调用字段对象的get方法,获得对应的字段值并判断该字段是否为空指针。
    如此一来,某个添加了非空注解的字段,要是它的字段值被检查出为空指针,马上就能断定包含该字段的对象是个无效记录。
//演示如何利用注解进行字段为空的校验
public class NullCheck {

	// 对指定对象进行空指针校验。返回true表示该对象跟它的每个字段都非空,返回false表示对象为空或者至少一个字段为空
	public static boolean isValid(Object obj) {
		if (obj == null) {
			System.out.println("校验对象为空");
			return false;
		}
		Class cls = obj.getClass(); // 获得对象实例的基因类型
		// 声明一个字符串清单,用来保存非空校验失败的无效字段名称
		List<String> invalidList = new ArrayList<String>();
		try {
			// 获取对象的所有属性(如果使用getFields,就无法获取到private的属性)
			Field[] fields = cls.getDeclaredFields();

			for (Field field : fields) { // 依次遍历每个对象属性
				// 如果该属性声明了NotNull注解,就进行字段非空的校验
				if (field.isAnnotationPresent(NotNull.class)) {
					if (field != null) {
						field.setAccessible(true); // 将该字段设置为允许访问
						Object value = field.get(obj); // 获取某实例的字段值
						if (value == null) { // 如果发现该字段为空
							// 就把该字段的名称添加到无效清单中
							invalidList.add(field.getName());
						}
					}
				}
			}
		} catch (Exception e) { // 捕捉到了任何一种异常(错误除外)
			e.printStackTrace();
		}
		if (invalidList.size() > 0) { // 无效清单非空,表示至少有一个字段没通过非空校验
			String desc = String.format("%s类非空校验不通过的字段有:%s",
					cls.getName(), invalidList.toString());
			System.out.println(desc);
			return false;
		} else {
			return true;
		}
	}
}

4. 在业务需要的地方调用校验方法

// 联合运用Optional校验、流式处理,以及注解校验
private static void getRedAppleByStreamWithNullCheck(List<Apple> list) {
	List<Apple> redAppleList = new ArrayList<Apple>();
	// ifPresent表示list非空时候的处理
	Optional.ofNullable(list).ifPresent(apples -> {
		// 从原始清单中筛选出红苹果清单。注意“NullCheck::isValid”为静态方法引用的写法
		redAppleList.addAll(apples.stream().filter(NullCheck::isValid).filter(Apple::isRedApple).collect(Collectors.toList()));
	});
	System.out.println("流式处理,非空校验之后的红苹果清单=" + redAppleList.toString());
}

17、文件读写

17.1 文件与目录的管理

参考文章

1、检查文件状态

File工具既可操作某个文件,也可操作某个目录。

方法 说明
exists 判断当前文件/目录是否存在,存在返回true,不存在返回false。
canExecute 判断当前文件是否允许执行,允许返回true,不允许返回false。
canRead 判断当前文件是否允许读取,允许返回true,不允许返回false。
canWrite 判断当前文件是否允许写入,允许返回true,不允许返回false。
isHidden 判断当前文件/目录是否隐藏,隐藏返回true,没隐藏返回false。
isDirectory 判断当前是否为目录,是返回true,否返回false。
isFile 判断当前是否为文件,是返回true,否返回false。

2、获取文件信息

只要磁盘中存在某个文件/目录

方法 说明
getAbsolutePath 获取当前文件/目录的绝对路径。
getPath 获取当前文件/目录的相对路径。
getName 如果当前为文件,则返回文件名称;如果当前为目录,则返回目录名称。
getParent 获取当前文件/目录的上级目录路径。
length 如果当前为文件,则返回文件大小;如果当前为空目录,则返回0;如果当前目录非空,则返回该目录的索引空间大小,索引保存了目录内部的文件基本信息。
lastModified 获取当前文件/目录的最后修改时间,单位毫秒。

3、管理文件操作

方法 说明
mkdir 只创建最后一级目录,如果上级目录不存在就返回false。
mkdirs 创建文件路径中所有不存在的目录。
createNewFile 创建新文件。如果文件路径中的目录不存在,就会扔出异常IOException。
delete 删除文件,也可删除空目录,但不可删除非空目录。在删除非空目录时会返回false。
renameTo 文件重命名,把源文件的名称改为目标名称。

4、遍历某目录下的文件

  1. list:返回的是String类型的文件路径数组
  2. listFiles:返回的是Fille类型的文件路径数组
重载方法 返回内容
listFiles() 当前目录下的所有文件和目录
listFiles(FileFilter fileFilter) 根据文件信息筛选符合条件的文件和目录
listFiles(FilenameFilter filenameFilter) 根据文件信息和文件名称筛选符合条件的文件和目录,经常用于过滤特定扩展名的文件

注意:FileFilter与FilenameFilter都属于函数式接口,所以它们的实例可以采用Lambda表达式来改写。
检查文件是否以“.txt”结尾即可判断它是否为文本文件示例如下:

File path = new File(mPath);
File[] txts;
// 匿名内部类的写法。通过文件名称过滤器FilenameFilter来筛选文件
txts = path.listFiles(new FilenameFilter() {
	@Override
	public boolean accept(File dir, String name) {
		return name.toLowerCase().endsWith(".txt"); // 文件扩展名为txt
	}
});

Lambda表达式优化;

// Lambda表达式的写法
txts = path.listFiles((dir, name) -> name.toLowerCase().endsWith(".txt"));

17.2 通过字符流读写文件

参考文章

对于写操作来说,需要通过文件写入器FileWriter搭配File工具才行。创建写入器对象的过程很简单,只要在调用FileWriter的构造方法时传递文件对象即可,接着就能调用写入器的下列方法向文件写入数据了。

方法 说明
write 往文件写入字符串。注意该方法存在多个同名的重载方法
append 也是往文件写入字符串。按字面意思,append方法像是往文件末尾追加字符串,然而并非如此,append方法与write方法的写入位置是同样的。二者的区别在于,append方法会把空指针当作“null”写入文件,而write方法不支持写入空指针
close 关闭文件写入器

Java7开始,try语句支持“try-with-resources”的表达式,意思是携带某些资源去尝试干活,并在尝试结束后自动释放这些资源。具体做法是 在try后边添加圆括号,并在圆括号内部填写资源对象的创建语句,只要这个资源类实现了AutoCloseable接口,程序便会在try/catch结束后自动调用该资源的close方法,由该资源类繁衍而来的所有子类都具备自动释放资源的功能。 这样就无需补充finally代码块,也无需显式调用close方法了,采取资源自动管理的优化代码如下所示:

// 采取自动释放资源的写文件代码
private static void writeFileWithTry() {
	String str = "白日依山尽,黄河入海流。\n";
	File file = new File(mFileName); // 创建一个指定路径的文件对象
	// Java7的新增功能,在try(...)里声明的资源,会在try/catch结束后自动释放。
	// 相当于编译器自动补充了finally代码块中的资源释放操作。
	// 资源类必须实现java.lang.AutoCloseable接口,这样close方法才会由系统调用。
	// 一般说来,文件I/O、套接字、数据库连接等均已实现该接口。
	try (FileWriter writer = new FileWriter(file)) {
		writer.write(str); // 往文件写入字符串
	} catch (IOException e) {
		e.printStackTrace();
	}
}

文件读取器FileReader,创建读取器对象也要在调用FileReader的构造方法时传递文件对象,读取器提供的调用方法列举如下:

方法 说明
skip 跳过若干字符。注意FileReader的skip方法跳过的是字符数,不是字节数。
read 从文件读取数据到字节数组。注意该方法存在多个同名的重载方法。
close 关闭文件读取器。

17.3 通过缓冲区读写文件

参考文章
BufferedWriter还新增了下列几个方法:

方法 说明
newLine 往文件末尾添加换行标记(Window系统是回车加换行)。当然实际上是先往缓存添加换行标记,并非直接往磁盘写入换行标记。
flush 立即将缓冲区中的数据写入磁盘。默认情况要等缓冲区满了才会写入磁盘,或者调用close方法关闭文件之时也会写入磁盘,但是有时程序猴急,一定要立即写入磁盘,此时就需调用flush方法强行写磁盘。

BufferedWriter使用步骤:

  1. 创建文件读取器对象,并获得父类Writer的实例
  2. 再据此创建缓存写入器对象。

通过缓存写入器把多行字符串写入文件的代码例子:

private static String mSrcName = "D:/test/aad.txt";
// 使用缓存字符流写入文件
private static void writeBuffer() {
	String str1 = "白日依山尽,黄河入海流。";
	String str2 = "欲穷千里目,更上一层楼。";
	File file = new File(mSrcName); // 创建一个指定路径的文件对象
	// try(...)允许在圆括号内部拥有多个资源创建语句,语句之间以冒号分隔
	// 先创建文件写入器,再根据文件读取器创建缓存写入器
	try (Writer writer = new FileWriter(file);
			BufferedWriter bwriter = new BufferedWriter(writer);) {
		// FileWriter的每次write调用都会直接写入磁盘,不但效率低,性能也差。
		// BufferedWriter的每次write调用会先写入缓冲区,直到缓冲区满了才写入磁盘,
		// 缓冲区大小默认是8K,查看源码defaultCharBufferSize = 8192;
		// 资源释放的close方法再把缓冲区的剩余数据写入磁盘,
		// 或者中途调用flush方法也可提前将缓冲区的数据写入磁盘。
		bwriter.write(str1); // 往文件写入字符串
		bwriter.newLine(); // 另起一行,也就是在文件末尾添加换行标记(Window系统是回车加换行)
		bwriter.write(str2);  // 往文件写入字符串
		//bwriter.flush(); // 把缓冲区中的数据写入磁盘
	} catch (Exception e) {
		e.printStackTrace();
	}
}

BufferedReader比起文件读取器新增了如下方法:

方法 说明
readLine 从文件中读取一行数据。
mark 在当前位置做个标记。
reset 重置文件指针,令其回到上次标记的位置。也就是回到上次mark方法标记的文件位置。
lines 读取文件内容的所有行,返回的是Stream流对象,之后便可按照流式处理来加工该字符串流。

BufferedReader使用步骤:

  1. 先创建文件读取器
  2. 根据其父类的读取器实例创建缓存读取器。

下面是通过缓存读取器从文件中读取多行字符串的代码例子:

// 使用缓存字符流读取文件
private static void readBuffer() {
	File file = new File(mSrcName); // 创建一个指定路径的文件对象
	// try(...)允许在圆括号内部拥有多个资源创建语句,语句之间以冒号分隔
	// 先创建文件读取器,再根据文件读取器创建缓存读取器
	try (Reader reader = new FileReader(file);
			BufferedReader breader = new BufferedReader(reader);) {
		breader.mark((int) file.length()); // 做个标记
		for (int i=1; ; i++) {
			// FileReader只能一个字符一个字符地读,或者一次性读进字符数组。
			// BufferedReader还支持一行一行地读。
			String line = breader.readLine(); // 从文件中读出一行文字
			if (line == null) { // 读到了空指针,表示已经到了文件末尾
				break;
			}
			System.out.println("第"+i+"行的文字为:"+line);
		}
		breader.reset(); // 重置文件指针,令其回到上次标记的位置
		for (int i=1; ; i++) {
			String line = breader.readLine(); // 从文件中读出一行文字
			if (line == null) { // 读到了空指针,表示已经到了文件末尾
				break;
			}
			System.out.println("又读了一遍 第"+i+"行的文字为:"+line);
		}
		//breader.lines(); // 返回Stream对象,之后可按照流式处理来加工该字符串流
	} catch (Exception e) {
		e.printStackTrace();
	}
}

17.4 采用缓存读取器和缓存写入器逐行复制

参考文章
通过缓存字符流逐行复制文件

private static String mSrcName = "D:/test/aad.txt";
private static String mDestName = "D:/test/aad_copy.txt";
// 通过缓存字符流逐行复制文件
private static void copyFile() {
	File src = new File(mSrcName); // 创建一个指定路径的源文件对象
	File dest = new File(mDestName); // 创建一个指定路径的目标文件对象
	// try(...)允许在圆括号内部拥有多个资源创建语句,语句之间以冒号分隔
	// 分别创建源文件的缓存读取器,以及目标文件的缓存写入器
	try (BufferedReader breader = new BufferedReader(new FileReader(src));
			BufferedWriter bwriter = new BufferedWriter(new FileWriter(dest));) {
		for (int i=0; ; i++) {
			String line = breader.readLine(); // 从文件中读出一行文字
			if (line == null) { // 读到了空指针,表示已经到了文件末尾
				break;
			}
			if (i != 0) { // 第一行开头不用换行
				bwriter.newLine(); // 另起一行,也就是在文件末尾添加换行标记
			}
			bwriter.write(line); // 往文件写入字符串
		}
	} catch (Exception e) {
		e.printStackTrace();
	}
	System.out.println("文件复制完成,源文件大小="+src.length()+",新文件大小="+dest.length());
}

通过缓存字符流逐个字符复制文件

// 通过缓存字符流逐个字符复制文件
private static void copyFileByInt() {
	File src = new File(mSrcName); // 创建一个指定路径的源文件对象
	File dest = new File(mDestName); // 创建一个指定路径的目标文件对象
	// try(...)允许在圆括号内部拥有多个资源创建语句,语句之间以冒号分隔
	// 分别创建源文件的缓存读取器,以及目标文件的缓存写入器
	try (BufferedReader breader = new BufferedReader(new FileReader(src));
			BufferedWriter bwriter = new BufferedWriter(new FileWriter(dest));) {
		while (true) { // 开始遍历文件中的所有字符
			int temp = breader.read(); // 从源文件中读出一个字符
			if (temp == -1) { // read方法返回-1表示已经读到了文件末尾
				break;
			}
			bwriter.write(temp); // 往目标文件写入一个字符
		}
	} catch (Exception e) {
		e.printStackTrace();
	}
	System.out.println("文件复制完成,源文件大小="+src.length()+",新文件大小="+dest.length());
}

需要注意的是,使用字符流复制文件只有逐行复制和逐字符复制两种方式,不可采取整个读到字符数组再整个写入字符数组的方式。之所以不能通过字符数组复制文件,是因为中文跟英文不一样,一个汉字会占用多个字节(GBK编码的每个汉字占用两个字节,UTF8编码的每个汉字占用三个字节)。若要把文件内容读到字符数组,势必先得知晓该数组的长度,可是调用文件对象的length方法只能得到该文件的字节长度,并非字符长度。譬如“白日依山尽”这个字符串在内存中的字符数组长度为5,写到UTF8编码的文件之后,文件大小是5*3=15字节;接着想把文件内容读到字符数组,然而15字节的文件天晓得它有几个字符,可能有5个UTF8编码的中文字符,也可能有15个英文字符,也可能有5个GBK编码的中文字符加5个英文字符共10个字符,总之你根本想不到该分配多大的字符数组。既然确定不了待读取的字符数组长度,就无法一字不差地复制文件内容了。

17.5 RandomAccessFile 随机访问文件的读写

参考文章

文件修改工具即RandomAccessFile(随机访问文件类),该工具特别适合对文件做各种花式修改。随机文件工具RandomAccessFile提供了seek方法用来定位当前的读写位置,可以很方便地在指定位置写入数据,故而RandomAccessFile经常用于以下几个场合:

  1. 往大文件末尾追加数据。
  2. 下载文件时候的断点续传,支持从上次已下载完成的地方中途开始,而不必重头下载整个文件。
    创建随机文件对象依然要指定文件路径,同时还要指定该文件的打开方式,下面是创建随机文件对象的代码例子:
// 根据文件路径创建既可读又可写的随机文件对象
String mAppendFileName = "D:/test/random_appendStr.txt";
RandomAccessFile raf = new RandomAccessFile(mAppendFileName, "rw");

上面构造方法的第二个参数值为rw,表示以既可读又可写的模式打开文件。除了常见的rw,模式参数还有其它取值,具体的取值说明如下:

参数值 说明
r 以只读方式打开指定文件。如果试图对该文件执行write写入方法,则会抛出异常IOException。
rw 以可读且可写的方式打开指定文件。如果该文件不存在,则尝试创建新文件。
rws 以可读且可写的方式打开指定文件。rws模式的每次write方法都会立即写入文件,它相当于FileWriter
rwd 与rws模式类似。区别在于rwd只更新文件内容,不更新文件的元数据,而rws模式会同时更新文件内容及元数据。所谓元数据保存了文件的基本信息,包括文件类型(是文件还是目录)、文件的创建时间、文件的修改时间、文件的访问权限(是否可读、是否可写、是否可执行)等等。

与字符流工具相比,随机文件工具用起来反而更简单,一个RandomAccessFile就集成了File、
FileWriter、FileReader三个工具的基本用法,它的主要方法说明如下:

方法 说明
length 获取指定文件的文件大小。
setLength 设置指定文件的文件大小。
seek 移动指定文件的访问位置。
write 往文件的当前位置写入字节数组。
read 把当前位置之后的文件内容读到字节数组。
close 关闭文件。RandomAccessFile拥有close方法,意味着它支持try-with-resources方式的自动释放资源。

在文件末尾追加数据为例,先调用seek方法定位到文件末尾,再调用write方法写入字节数组形式的数据。这个追加功能的实现代码如下所示:

private static String mAppendFileName = "D:/test/random_appendStr.txt";
// 往随机文件末尾追加字符串
private static void appendStr() {
	// 创建指定路径的随机文件对象(可读写)。try(...)支持在处理完毕后自动关闭随机文件
	try (RandomAccessFile raf = new RandomAccessFile(mAppendFileName, "rw")) {
		long length = raf.length(); // 获取随机文件的长度(文件大小)
		raf.seek(length); // 定位到指定长度的位置
		String str = String.format("你好世界%.10f\n", Math.random());
		raf.write(str.getBytes()); // 往随机文件写入字节数组
	} catch (Exception e) {
		e.printStackTrace();
	}
}

往文件内部的任意位置插入数据的例子,仍然是先调用seek方法跳到指定位置,再调用write方法写入字节数据。

private static String mFixsizeFileName = "D:/test/random_fixsize.txt";
// 往固定大小的随机文件中插入数据
private static void fixSizeInsert() {
	// 创建指定路径的随机文件对象(可读写)。try(...)支持在处理完毕后自动关闭随机文件
	try (RandomAccessFile raf = new RandomAccessFile(mFixsizeFileName, "rw")) {
		raf.setLength(1000); // 设置随机文件的长度(文件大小)
		for (int i=0; i<=2 ;i++) {
			raf.seek(i*200); // 定位到指定长度的位置
			String str = String.format("你好世界%.10f\n", Math.random());
			raf.write(str.getBytes()); // 往随机文件写入字节数组
		}
	} catch (Exception e) {
		e.printStackTrace();
	}
}

随机文件工具的读文件操作,与字符流工具比较,它俩的处理流程大体一致,但在细节上有个区别:随机文件工具的read方法支持一次性读到字节数组,而字符流工具的read方法支持一次性读到字符数组

// 读取随机文件的文件内容
private static void readContent() {
	// 创建指定路径的随机文件对象(只读)。try(...)支持在处理完毕后自动关闭随机文件
	try (RandomAccessFile raf = new RandomAccessFile(mAppendFileName, "r")) {
		int length = (int) raf.length(); // 获取随机文件的长度(文件大小)
		byte[] bytes = new byte[length]; // 分配长度为文件大小的字节数组
		raf.read(bytes); // 把随机文件的文件内容读取到字节数组
		String content = new String(bytes); // 把字节数组转成字符串
		System.out.println("content="+content);
	} catch (Exception e) {
		e.printStackTrace();
	}
}

18、I/O流

18.1 文件字节I/O流

参考文章
FileOutputStream用来将数据写入文件,FileInputStream用来从文件读取数据,并且二者都采取字节数组保存信息。不管是输出流还是输入流,它们都实现了AutoCloseable接口,故而支持try-with-resources方式的资源自动释放。

FileOutputStream写文件的代码例子:

private static String mFileName = "D:/test/aae.txt";
// 利用文件输出流写入文件。注意FileOutputStream处理的是字节信息
private static void writeFile() {
	String str = "白日依山尽,黄河入海流。\n欲穷千里目,更上一层楼。";
	// 根据指定路径构建文件输出流对象
	try (FileOutputStream fos = new FileOutputStream(mFileName)) {
		fos.write(str.getBytes()); // 把字节数组写入文件输出流
		// 在try(...)里面创建的输入输出流,程序会在处理完成后自动关闭,所以下面的close方法不必显式调用
		//fos.close(); // 关闭文件输出流
	} catch (Exception e) {
		e.printStackTrace();
	}
}

FileInputStream有几个方法值得一提:

方法 说明
skip 命令当前位置跳过若干字节,注意该方法跳过的是字节数而非字符数
available 返回文件当前位置后面的剩余部分大小,刚创建文件输入流对象之时调用available方法,得到的就是文件大小;如果先调用skip方法再调用available方法,得到的数值为文件大小减去跳过的字节数。

文件输入流读文件的代码例子:

// 利用文件输入流读取文件
private static void readFile() {
	// 根据指定路径构建文件输入流对象
	try (FileInputStream fis = new FileInputStream(mFileName)) {
		// 分配长度为文件大小的字节数组。available方法返回当前位置后面的剩余部分大小
		byte[] bytes = new byte[fis.available()];
		fis.read(bytes); // 从文件输入流中读取字节数组
		String content = new String(bytes); // 把字节数组转换为字符串
		System.out.println("content="+content);
		// 在try(...)里面创建的输入输出流,程序会在处理完成后自动关闭,所以下面的close方法不必显式调用
		//fis.close(); // 关闭文件输入流
	} catch (Exception e) {
		e.printStackTrace();
	}
}

18.2 缓存字节I/O流

参考文章

缓存输出流BufferedOutputStream:

  1. 每次创建缓存输出流对象之前,都要先构建文件输出流对象,然后据此构建缓存输出流对象;
  2. 它的write方法先把数据写到缓存,等到缓存满了才写入磁盘,或者在调用close方法关闭文件之时将缓存数据写入磁盘。
  3. 缓存输出流仍然提供了flush方法,调用flush方法表示立即把缓存中的数据写入磁盘。
    下面是利用缓存输出流写文件的代码例子:
private static String mSrcName = "D:/test/aaf.txt";
// 利用缓存输出流写入文件
private static void writeBuffer() {
	String str = "白日依山尽,黄河入海流。\n欲穷千里目,更上一层楼。";
	// 根据指定文件路径构建文件输出流对象,然后据此构建缓存输出流对象
	try (FileOutputStream fos = new FileOutputStream(mSrcName);
			BufferedOutputStream bos = new BufferedOutputStream(fos)) {
		bos.write(str.getBytes()); // 把字节数组写入缓存输出流
		//bos.flush(); // 立即写入磁盘。如果不立即写入,最后调用close方法时也会写入
	} catch (Exception e) {
		e.printStackTrace();
	}
}

缓存输入流BufferedInputStream:
BufferedInputStream保留了mark和reset两个方法:

方法 说明
mark 在当前位置做个标记
reset 重置输入流指针,令其回到上次标记的位置。
// 利用缓存输入流读取文件
private static void readBuffer() {
	// 根据指定文件路径构建文件输入流对象,然后据此构建缓存输入流对象
	try (FileInputStream fis = new FileInputStream(mSrcName);
			BufferedInputStream bis = new BufferedInputStream(fis)) {
		// 分配长度为文件大小的字节数组。available方法返回当前位置后面的剩余部分大小
		byte[] bytes = new byte[bis.available()];
		bis.read(bytes); // 从缓存输入流中读取字节数组
		// 缓存输入流的mark和reset用法类似于BufferedReader的同名方法
		//bis.mark(bis.available()); // 在当前位置做个标记
		//bis.reset(); // 重置输入流指针,令其回到上次标记的位置
		String content = new String(bytes); // 把字节数组转换为字符串
		System.out.println("content="+content);
	} catch (Exception e) {
		e.printStackTrace();
	}
}

18.3 通过缓存输入和输出流复制文件

参考文章
调用缓存输入流对象的read方法,将文件数据读到指定的字节数组;然后调用缓存输出流对象的write方法,马上把刚读取的字节数组写入文件,一进一出之间就顺带完成了文件复制功能。
下面是通过缓存输入和输出流复制文件的代码例子:

private static String mSrcName = "D:/test/aaf.txt";
private static String mDestName = "D:/test/aaf_copy.txt";
// 利用缓存输入和输出流复制文件
private static void copyFile() {
	// 分别构建缓存输入流对象和缓存输出流对象
	try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(mSrcName));
			BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(mDestName))) {
		// 分配长度为文件大小的字节数组。available方法返回当前位置后面的剩余部分大小
		byte[] bytes = new byte[bis.available()];
		bis.read(bytes); // 从缓存输入流中读取字节数组
		bos.write(bytes); // 把字节数组写入缓存输出流
		System.out.println("文件复制完成,源文件大小="+bytes.length+",新文件大小="+bytes.length);
	} catch (Exception e) {
		e.printStackTrace();
	}
}

18.4 序列化

参考文章

个人总结概要:

 1. 可序列化的类实现了Serializable接口;
 2. 可序列化的类需要给serialVersionUID字段赋值,避免出现版本编码不一致的情况;
 3. 可序列化的类可能有部分字段被关键字transient所修饰,表示这些字段无需进行序列化;

把对象转成磁盘文件可识别数据的过程,Java称之为“序列化”;反过来,把磁盘文件内容转成内存中对象的过程,Java称之为“反序列化”。

方法 说明
Object.readObjectInputStream 从文件中读取对象信息
ObjectOutputStream.writeObject 将对象信息写入文件

往文件写入序列化对象的代码例子:

private static String mFileName = "D:/test/user.txt";
// 利用对象输出流把序列化对象写入文件
private static void writeObject() {
	// 下面创建可序列化的用户信息对象,并给予赋值
	UserInfo user = new UserInfo();
	user.setName("王五");
	user.setPhone("15960238696");
	user.setPassword("111111");
	// 根据指定文件路径构建文件输出流对象,然后据此构建对象输出流对象
	try (FileOutputStream fos = new FileOutputStream(mFileName);
			ObjectOutputStream oos = new ObjectOutputStream(fos);) {
		oos.writeObject(user); // 把对象信息写入文件
		System.out.println("对象序列化成功");
	} catch (Exception e) {
		e.printStackTrace();
	}
}

从文件读取对象信息:

// 利用对象输入流从文件中读取序列化对象
private static void readObject() {
	// 创建可序列化的用户信息对象
	UserInfo user = new UserInfo();
	// 根据指定文件路径构建文件输入流对象,然后据此构建对象输入流对象
	try (FileInputStream fos = new FileInputStream(mFileName);
			ObjectInputStream ois = new ObjectInputStream(fos);) {
		user = (UserInfo) ois.readObject(); // 从文件读取对象信息
		System.out.println("对象反序列化成功");
	} catch (Exception e) {
		e.printStackTrace();
	}
	// 注意用户信息的密码字段设置了禁止序列化,故而文件读到的密码字段为空
	String desc = String.format("姓名=%s,手机号=%s,密码=%s", 
			user.getName(), user.getPhone(), user.getPassword());
	System.out.println("用户信息如下:"+desc);
}

18.5 IO流处理简单的数据压缩

参考文章

压缩与解压操作需要GZIPOutputStream、GZIPInputStream、ByteArrayOutputStream、ByteArrayInputStream这四个工具类互相配合。

工具类 说明 详细
GZIPOutputStream 压缩输出流 它吃进去的是原始数据的字节数组,拉出来的是字节数组输出流对象(压缩后的数据)。
ByteArrayOutputStream 字节数组输出流 它从压缩输出流获取压缩后的数据,并通过toByteArray方法输出字节数组信息。或者从压缩输入流获取解压后的数据,并通过toByteArray方法输出字节数组信息。
GZIPInputStream 压缩输入流 它吃进去的是字节数组输入流对象(压缩后的数据),拉出来的是解压后的字节数组(原始数据)。
ByteArrayInputStream 字节数组输入流 它输入压缩数据的字节数组,转成流对象后丢给压缩输入流。

19、NIO——非阻塞的IO

传统的流式IO又被称作BIO(“Blocking IO”的缩写),意即阻塞的IO。非阻塞模式只适用于网络请求交互。

19.1 文件通道的基本用法

文件通道中的数据允许双向流动,流进来意味着读操作,流出去意味着写操作,这样文件的读写操作集中在文件通道里进行,大大节省了系统的资源开销。
文件通道对应的Java类型名叫FileChannel,它的创建方式主要有两种:

  1. 调用输入输出流的getChannel方法
  2. 根据随机访问文件获得文件通道
// 第一种方式:根据文件输入流获得可读的文件通道
FileChannel channel1 = new FileInputStream(mFileName).getChannel();
//又如下面代码根据文件输出流得到了可写的文件通道:
// 第一种方式:根据文件输出流获得可写的文件通道
FileChannel channel2 = new FileOutputStream(mFileName).getChannel();
//第二种方式则要通过随机文件工具,仍旧调用随机文件工具的getChannel方法获取通道对象。此时文件通道对象的构建代码示例如下:
// 第二种方式:根据随机访问文件获得可读的文件通道
FileChannel channel1 = new RandomAccessFile(mFileName, "r").getChannel();
// 第二种方式:根据随机访问文件获得可写的文件通道
FileChannel channel2 = new RandomAccessFile(mFileName,"rw").getChannel();
方法 说明
isOpen 判断文件通道是否打开。
size 获取文件通道的大小(即文件长度)。
truncate 截断文件大小到指定长度。
read 把文件通道中的数据读到字节缓存。
write 往文件通道写入字节缓存中的数据。
force 强制写入磁盘,相当于缓存输出流的flush方法。
close 关闭文件通道。

文件通道写文件:

// 通过文件通道写入文件
private static void writeChannel() {
	String str = "春眠不觉晓,处处闻啼鸟。\n夜来风雨声,花落知多少。";
	// 根据文件输出流获得可写的文件通道。注意文件通道支持try(...)的自动关闭操作
	try (FileChannel channel = new FileOutputStream(mFileName).getChannel()) {
		// 生成字符串对应的字节缓存对象
		ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
		channel.write(buffer); // 往文件通道写入字节缓存
		//channel.force(true); // 强制写入磁盘,相当于输出流的flush方法
	} catch (Exception e) {
		e.printStackTrace();
	}
}

文件通道读文件:

// 通过文件通道读取文件
private static void readChannel() {
	// 根据文件输入流获得可读的文件通道。注意文件通道支持try(...)的自动关闭操作
	try (FileChannel channel = new FileInputStream(mFileName).getChannel()) {
		int size = (int) channel.size(); // 获取文件通道的大小(即文件长度)
		// 分配指定大小的字节缓存
		ByteBuffer buffer = ByteBuffer.allocateDirect(size);
		channel.read(buffer); // 把文件通道中的数据读到字节缓存
		buffer.flip(); // 把缓冲区从写模式切换到读模式。从缓冲区读取数据之前,必须先调用flip方法
		byte[] bytes = new byte[size]; // 创建与文件大小相同长度的字节数组
		buffer.get(bytes); // 把字节缓存中的数据取到字节数组
		String content = new String(bytes); // 把字节数组转换为字符串
		System.out.println("content="+content);
	} catch (Exception e) {
		e.printStackTrace();
	}
}

19.2 深入理解字节缓存ByteBuffer

参考文章

字节缓存ByteBuffer位于通道内部的存储空间,也是通道唯一可用的存储形式。
ByteBuffer有两种构建方式:

  1. 调用静态方法wrap,根据输入的字节数组生成对应的缓存对象
  2. 调用静态方法allocateDirect,根据输入的数值分配指定大小的空缓存。
  3. 调用FileChannel工具的open方法(详见19.4,基本采取前两种方式,很少使用Path工具的第三种方式。)

字节缓存又是一种特殊的存储空间,因为它可能会被多次读写,所以为了有效地控制读写操作,Java给它设计了下列五种概念:容量、当前限制量、当前位置、本次剩余空间、标记位置。
动作①和动作②实现了将字符串写入文件的功能,动作③和动作④实现了将文件内容读到字符串的功能。

Java基础知识总结_第1张图片

ByteBuffer的方法:

方法 说明
clear 缓冲区数据写入通道之后,如果还想把新数据写入缓冲区,就要先调用clear方法清空它。
compact 只清除已经读过的数据,剩余的未读数据会移到缓冲区开头,新增的数据将加到未读数据后面。
flip 把缓冲区从写模式切换到读模式。从缓冲区读取数据之前,必须先调用flip方法
rewind 让缓冲区的指针回到开头,以便重新再来一遍。

每个方法在调用之后将会引起参数的变化如下表:

方法 position limit mark
clear 0 容量大小 -1
compact 0 容量大小 -1
flip 0 上次的当前位置 -1
rewind 0 保持不变 -1

就具体的代码逻辑而言,一般在写入字节缓存之前(上图的动作①与动作③),需要先调用compact方法;在读取字节缓存之前(上图的动作②与动作④),需要先调用flip方法。当然如果是创建字节缓存后的第一次操作,就不必调用compact方法或者flip方法,因为在一开始字节缓存的当前位置都是指向0,无需再将当前位置挪回缓存开头了。

int size = (int) channel.size(); // 获取文件通道的大小(即文件长度)
// 分配指定大小的字节缓存
ByteBuffer buffer = ByteBuffer.allocateDirect(size);
channel.read(buffer); // 把文件通道中的数据读到字节缓存
buffer.flip(); // 把缓冲区从写模式切换到读模式。从缓冲区读取数据之前,必须先调用flip方法
byte[] bytes = new byte[size]; // 创建与文件大小相同长度的字节数组
buffer.get(bytes); // 把字节缓存中的数据取到字节数组

19.3 文件通道的性能优势

参考文章

传统IO复制文件的完整数据流程正如下图所示:
Java基础知识总结_第2张图片
通道本身是专门负责I/O操作的处理机,字节缓存又是通道内部的存储空间,故而利用通道复制文件的话,既无需动用操作系统的系统内存,也无需动用应用程序的应用内存。使用文件通道数据流转过程如下图所示:
Java基础知识总结_第3张图片
针对文件复制功能,由于已经明确要把源文件的全部内容完成写入新文件,因此不必显式通过字节缓存完成数据的读取与写入动作,可以直接调用通道对象的transferTo方法或者transferFrom方法完成文件复制。其中transferTo方法操作的是源文件通道,它把数据传给目标文件通道;transferFrom方法操作的是目标文件通道,它从源文件通道传入数据。详细的调用代码例子如下所示:

// 使用文件通道直接复制文件
private static void copyChannelDirect() {
	// 分别创建源文件的文件通道,以及目标文件的文件通道
	try (FileChannel src = new FileInputStream(mSrcName).getChannel();
			FileChannel dest = new FileOutputStream(mDestName).getChannel();) {
		// 下面的transferTo和transferFrom都可以完成文件复制功能,选择其中一个即可
		src.transferTo(0, src.size(), dest); // 操作源文件通道,把数据传给目标文件通道
		//dest.transferFrom(src, 0, src.size()); // 操作目标文件通道,从源文件通道传入数据
	} catch (Exception e) {
		e.printStackTrace();
	}
}

19.4 NIO配套的文件工具Files

参考文章

Path的方法 说明
getParent 获取当前路径所在的上级目录的Path对象。
resolve 拼接文件路径,在当前路径的末尾添加指定字符串,并返回新的文件路径。
startsWith 判断当前路径是否以指定字符串开头。
endsWith 判断当前路径是否以指定字符串结尾。
toString 获取当前路径对应的名称字符串。
toFile 获取当前路径对应的File对象。
Paths的方法 说明
exists 判断该路径是否存在。
isDirectory 判断该路径是否为目录。
isExecutable 判断该路径是否允许执行。
isHidden 判断该路径是否隐藏。
isReadable 判断该路径是否可读。
isWritable 判断该路径是否可写。
size 获取该路径的文件大小。如果该路径是文件,则返回文件大小;如果该路径是目录,则返回目录基本信息的大小,而非整个目录的大小。
createDirectory 如果该路径是个目录,就创建新目录。
createFile 如果该路径是个文件,就创建新文件。
delete 如果该路径是文件或者空目录,就把它删掉。如果该路径不存在或者目录非空,就扔出异常。
deleteIfExists 如果该路径是文件或者空目录,就把它删掉(路径不存在也不报错)。但若目录非空,还是扔出异常。
copy 把文件从源路径复制到目标路径。
move 把文件从源路径移动到目标路径。

另外,Java8又给Files工具增加了以下几个方法,使之具备流式处理的能力:

Paths方法(java8) 说明
readAllLines 获取该文件的所有内容行,返回的是字符串清单。
lines 获取该文件的所有内容行,返回的是字符串流Stream。
list 获取该目录下的所有文件与目录,但不包括子目录的下级内容,返回的是路径流Stream。
walk 获取该目录下的所有文件与目录,且包括指定深度子目录的下级内容,返回的是路径流Stream。

具体用法:

  1. 通过Path打开文件通道
    调用FileChannel工具的open方法,根据传入的Path对象也能获得通道对象。。不加选项参数的open方法,默认得到只读的文件通道;若要得到可写的文件通道,则需给open方法传入选项参数StandardOpenOption.WRITE。需要注意的是,通过Path打开可写的文件通道有个问题:要是文件通道指向的文件路径并不存在,那么往该通道写入数据将会扔出异常,而非默认创建新文件。因而获取可写的文件通道之前必须添加检查代码,即判断指定路径是否存在,倘若该路径不存在,则要创建一个新文件。
  2. 遍历指定目录下(不包含子目录)的所有文件与目录
    调用Files工具的list方法即可实现指定目录(不包含子目录)的遍历功能,list方法返回的遍历结果为字符串流。比如要统计指定目录下面文件与目录数量,则先调用list方法获得字符串流对象,再调用count方法就能得到统计数目
  3. 遍历指定目录下(包含子目录)的所有文件与目录
    调用Files工具的walk方法,该方法支持设定待遍历的子目录深度(从当前目录往下数的目录层数)。
    walk方法与list方法同样返回的都是流对象,所以流式处理的filter、map、collect等方法统统适用,非常方便对某目录下的所有实体进行筛选操作。例如打算遍历指定目录以及深度在五之内的子目录,并返回其下所有目录的路径名称清单,利用walk方法实现的筛选代码是下面这样的:
try {
	// 根据指定的文件路径字符串获得对应的Path对象
	Path path = Paths.get(mDirName);
	// 遍历该目录以及深度在五之内的子目录,并返回其下所有目录的路径名称清单
	List<String> dirs = Files.walk(path, 5)
			.filter(Files::isDirectory) // 只挑选目录
			.map(it -> it.toString()) // 获取目录的路径名称
			.collect(Collectors.toList()); // 返回清单格式
	System.out.println("dirs="+dirs);
} catch (Exception e) {
	e.printStackTrace();
}

20、线程

20.1 线程基本用法

个人总结概要:
	
	1. 创建线程三种方式:
		 1)继承Thread方法
		 2)实现Runnable接口,重写run方法
		 3)利用Callable启动线程,重写call方法

参考文章

  • 多进程仿佛孙悟空拔毫毛变出许多小孙悟空,多线程仿佛哪吒变出三头六臂。
  • 调用了join方法的线程,它们的内部代码相较其它线程会优先处理。优先的意思并非一定会插到前面,而是尽量安排先执行,最终的执行顺序还得由操作系统来决定。
  • Thread类提供了优先级设置方法setPriority,调用该方法即可指定每个线程的优先级大小,数值越大的表示它拥有越高的优先级。

20.2 利用Runnable启动线程

个人总结概要:

 1. 继承Thread不能共享资源,实现Runnable接口可以共享资源

参考文章
采取匿名内部类的方式在线程类Thread的构造方法中直接填入实现后的Runnable任务代码,具体的调用代码如下所示:

// 通过Runnable创建线程的第二种方式:传入匿名内部类的实例
new Thread(new Runnable() {
	@Override
	public void run() {
		int product = 1;
		for (int i=1; i<=10; i++) {
			product *= i;
		}
		PrintUtils.print(Thread.currentThread().getName(), "阶乘结果="+product);
	}
}).start(); // 创建并启动线程

由于Runnable是函数式接口,因此完全可以使用Lambda表达式加以简化:

// 通过Runnable创建线程的第三种方式:使用Lambda表达式
new Thread(() -> {
	int product = 1;
	for (int i=1; i<=10; i++) {
		product *= i;
	}
	PrintUtils.print(Thread.currentThread().getName(), "阶乘结果="+product);
}).start(); // 创建并启动线程

20.3 利用Callable启动线程

参考文章

使用步骤:

  1. 创建Callable的实现类,并重写call()方法,该方法为线程执行体,并且该方法有返回值
  2. 创建Callable的实例,把Callable实例填进FutureTask的构造方法,由此得到一个未来任务的实例
  3. 调用未来任务的run方法启动该任务
  4. 调用未来任务的get方法获取任务的执行结果
  5. 把未来任务的实例放入Thread的构造方法当中,然后再调用线程实例的start方法方可启动线程任务。

FutureTask的主要方法:

方法 说明
run 启动未来任务。
get 获取未来任务的执行结果。
isDone 判断未来任务是否执行完毕。
cancel 取消未来任务。
isCancelled 判断未来任务是否取消。
// 进一步精简后的Lambda表达式
Callable<Integer> callable = () -> new Random().nextInt(100);
// 根据代码段实例创建一个未来任务
FutureTask<Integer> future = new FutureTask<Integer>(callable);
// 把未来任务放入新创建的线程中,并启动分线程处理
new Thread(future).start();
try {
	Integer result = future.get(); // 获取未来任务的执行结果
	PrintUtils.print(Thread.currentThread().getName(), "主线程的执行结果="+result);
} catch (InterruptedException | ExecutionException e) {
	// get方法会一直等到未来任务的执行完成,由于等待期间可能收到中断信号,因此这里得捕捉中断异常
	e.printStackTrace();
}

20.4 定时器与定时任务

参考链接

定时任务使用步骤:

  1. 继承TimeTask类,重写run方法
  2. 使用定时器Timer调度定时任务

Timer的调度方法:

方法 说明
带两个参数的schedule方法 其中第一个参数为定时任务,第二个参数为任务的启动时间或者延迟启动间隔。这种schedule方法只会启动惟一一次定时任务。
带三个参数的schedule方法 其中第一个参数为定时任务,第二个参数为任务的启动时间或者延迟启动间隔,第三个参数为之后继续启动的时间间隔。这种schedule方法会持续不断地启动定时任务。下个任务要在上个任务结束之后再间隔若干时间才启动,下次启动时间与任务执行耗时有关
scheduleAtFixedRate 其中第一个参数为定时任务,第二个参数为任务的启动时间或者延迟启动间隔,第三个参数为之后每次启动的时间间隔。scheduleAtFixedRate方法也会持续不断地启动定时任务。下个任务不管上个任务何时结束,只要相互之间的启动间隔到达,即可立即启动下个任务。下次启动时间与任务耗时无关

定时任务TimerTask和定时器Timer都提供了cancel方法TimerTask的cancel方法取消的是原来的定时任务,取消之后,还能通过定时器来调度新创建的定时任务。而Timer的cancel方法取消的是定时器自身,一旦取消定时器,那么不但原来的定时任务被一块撤销,而且该定时器不能再调度任何一个定时任务,相当于这个定时器彻底报废了,除非再次创建全新的定时器才能开展调度工作。

21、并发

21.1 线程同步synchronized

个人总结概要:
	
	 1. synchronized同步代码:同一时刻只能由一个线程操作该代码。该线程操作完之后,其他线程才可以进行操作。
	 2. synchronized并不是直接修饰线程的类的run方法,这样会导致该线程先执行完再执行其它线程。而是对run方法中值发生变化的部分修饰。

参考文章

21.2 通过加解锁避免资源冲突

参考文章

synchronized的方式局限性:

  1. synchronized必须用于修饰方法或者代码块,也就是一定会有花括号把需要同步的代码给包裹起来。这样的话,花括号内外的变量交互比较麻烦,特别是同步代码块,多出来的花括号硬生生把原来的代码隔离开,只好通过局部变量来传递数值。
  2. synchronized的同步方式很傻,一旦同步方法/代码块被某个线程执行,其它线程到了这里就必须等待前个线程的处理,要是前个线程迟迟不退出同步方法/代码块,那么其它线程只能傻傻的一直等下去。
  3. synchronized无法判断当前线程处于等待队列中的哪个位置,等待队列要是很长的话,也许走另外一条分支更合适,但synchronized是个死脑筋,它不知道等待队列的详细情况,也就无从选择更优的代码路径。

为此Java又设计了一套锁机制,通过锁的对象把加锁和解锁操作分离开,从而解决同步方式的弊端。锁机制提供了好几把锁,最常见的名叫可重入锁ReentrantLock,所谓可重入,字面意思指的是支持重新进入,凡是遇到被当前线程自身锁住的代码,则仍然允许进入这块代码;但要是遇到被其它线程锁住的代码,则不允许进入那块代码。换句话说,加锁不是为了锁自己,加锁是为了锁别人,故而可重入锁又称作自旋锁,之前介绍的synchronized也属于可重入机制。

ReentrantLock相关的锁方法:

方法 说明
lock 对可重入锁加锁。
unlock 对可重入锁解锁。
tryLock 尝试加锁。加锁成功返回true,加锁失败返回false。该方法与lock的区别在于:lock方法会一直等待加锁,而tryLock要求立刻加锁,要是加锁失败(表示之前已经被其它线程加了锁),就马上返回false,一会都等不了。
isLocked 判断该锁是否被锁住了。
getQueueLength 获取有多少个线程正在等待该锁的释放。

Java提供的读写锁工具名叫ReentrantReadWriteLock,意即可重入的读写锁,调用读写锁对象的readLock方法可获得读锁对象,调用读写锁对象的writeLock方法可获得写锁对象,之后再根据实际情况分别对读锁或者写锁进行加锁和解锁操作。

// 创建一个可重入的读写锁
private final static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
// 获取读写锁中的写锁
private final static WriteLock writeLock = readWriteLock.writeLock();
// 获取读写锁中的读锁
private final static ReadLock readLock = readWriteLock.readLock();

// 测试通过读写锁避免资源冲突
private static void testReadWriteLock() {
	Runnable seller = new Runnable() {
		private Integer ticketCount = 100; // 可出售的车票数量
		
		@Override
		public void run() {
			while (ticketCount > 0) { // 还有余票可供出售
				int count = 0;
				// 根据指定路径构建文件输出流对象
				try (FileOutputStream fos = new FileOutputStream(mFileName)) {
					readLock.lock(); // 对读锁加锁。加了读锁之后,其它线程可以继续加读锁,但不能加写锁
					if (ticketCount <= 0) { // 余票数量为0,表示已经卖光了,只好关门歇业
						fos.close(); // 关闭文件
						break; // 跳出售票的循环
					}
					readLock.unlock(); // 对读锁解锁
					writeLock.lock(); // 对写锁加锁。一旦加了写锁,则其它线程在此既不能读也不能写
					count = --ticketCount; // 余票数量减一
					writeLock.unlock(); // 对写锁解锁
					fos.write(new String(""+count).getBytes()); // 把字节数组写入文件输出流
				} catch (Exception e) {
					e.printStackTrace();
				}
				// 以下打印售票日志,包括售票时间、售票线程、当前余票等信息
				String left = String.format("当前余票为%d张", count);
				PrintUtils.print(Thread.currentThread().getName(), left);
			}
		}
	};
	new Thread(seller, "售票线程A").start(); // 启动售票线程A
	new Thread(seller, "售票线程B").start(); // 启动售票线程B
	new Thread(seller, "售票线程C").start(); // 启动售票线程C
}

21.3 线程间的通信方式

个人总结概要:
	
 1. 要想在线程间进行通信,就必须启用圆括号参数,并且两个线程都要在synchronized后面填写该参数对象。加锁:synchronized(参数对象实例),等待:参数对象实例.wait();	释放锁:参数对象实例.notify()或notifyAll();

参考文章

22、线程池

22.1 普通线程池的运用

个人总结概要:
	
	 1. 线程池就是对线程的调度管理。
	 2. 使用线程池管理线程步骤:
	 	 (1)创建线程任务(继承Runnable接口,重写run方法)
	 	 (2)创建线程池对象实例
	 	 (3)使用线程池对象实例的execute方法,将线程任务加入线程池

参考文章

线程池封装了线程的创建、启动、关闭等操作,以及系统的资源分配与线程调度;它还支持任务的添加和移除功能,使得程序员可以专心编写任务代码的业务逻辑,不必操心线程怎么跑这些细枝末节。

Java提供的线程池工具最常用的是ExecutorService及其派生类ThreadPoolExecutor,它支持以下四种线程池类型:

  1. 只有一个线程的线程池
// 创建一个只有一个线程的线程池
ExecutorService pool = (ExecutorService) Executors.newSingleThreadExecutor();
  1. 拥有固定数量线程的线程池
// 创建一个线程数量为3的线程池
ExecutorService pool = (ExecutorService) Executors.newFixedThreadPool(3);
  1. 拥有无限数量线程的线程池
// 创建一个不限制线程数量的线程池
ExecutorService pool = (ExecutorService) Executors.newCachedThreadPool();
  1. 线程数量允许变化的线程池
// 创建一个自定义规格的线程池(最小线程个数为2,最大线程个数为5,每个线程保持活跃的时长为60,时长单位秒,等待队列大小为19)
ThreadPoolExecutor pool = new ThreadPoolExecutor(
		2, 5, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(19));

说明如下:
第一个参数是个整型数,名叫corePoolSize,它指定了线程池的最小线程个数
第二个参数也是个整型数,名叫maximumPoolSize,它指定了线程池的最大线程个数
第三个参数是个长整数,名叫keepAliveTime,它指定了每个线程保持活跃的时长,如果某个线程的空闲时间超过这个时长,则该线程会结束运行,直到线程池中的线程总数等于corePoolSize为止。
第四个参数为TimeUnit类型,名叫unit,它指定了第三个参数的时间单位,比如TimeUnit.SECONDS表示时间单位是秒。
第五个参数为BlockingQueue类型,它指定了待执行线程所处的等待队列

创建好了线程池之后,即可调用线程池对象的execute方法将指定任务加入线程池。execute方法并不一定立刻执行指定任务,只有当线程池中存在空闲线程或者允许创建新线程之时,才会马上执行任务;否则会将该任务放到等待队列,然后按照排队顺序在方便的时候再一个一个执行队列中的任务。

ExecutorService的方法:

方法 说明
execute 将指定任务加入线程池
getCorePoolSize 获取核心的线程个数(即线程池的最小线程个数)。
getMaximumPoolSize 获取最大的线程个数(即线程池的最大线程个数)。
getPoolSize 获取线程池的当前大小(即线程池的当前线程个数)。
getTaskCount 获取所有的任务个数。
getActiveCount 获取活跃的线程个数。
getCompletedTaskCount 获取已完成的任务个数。
remove 从等待队列中移除指定任务。
shutdown 关闭线程池。关闭之后不能再往线程池中添加任务,不过要等已添加的任务执行完,才最终关掉线程池。用完后要关闭线程池,否则容易导致内存一直被消耗。
shutdownNow 立即关闭线程池。之后同样不能再往线程池中添加任务,同时会给已添加的任务发送中断信号,直到所有任务都退出才最终关掉线程池。
isShutdown 判断线程池是否已经关闭。

自定义的线程池,它的实验代码示例如下:

// 定义一个操作任务
private static class Operation implements Runnable {
	private String name; // 任务名称
	private int index; // 任务序号
	public Operation(String name, int index) {
		this.name = name;
		this.index = index;
	}
	
	@Override
	public void run() {
		// 以下打印操作日志,包括操作时间、操作线程、操作描述等信息
		String desc = String.format("%s执行到了第%d个任务", name, index+1);
		PrintUtils.print(Thread.currentThread().getName(), desc);
	}
};

//测试自定义的线程池
private static void testCustomPool() {
	// 创建一个自定义规格的线程池(最小线程个数为2,最大线程个数为5,每个线程保持活跃的时长为60,时长单位秒,等待队列大小为19)
	ThreadPoolExecutor pool = new ThreadPoolExecutor(
			2, 5, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(19));
	for (int i=0; i<10; i++) { // 循环启动10个任务
		// 创建一个操作任务
		Operation operation = new Operation("自定义的线程池", i);
		pool.execute(operation); // 命令线程池执行该任务
	}
	pool.shutdown(); // 关闭线程池
}
结果:
	22:28:46.337 pool-1-thread-1 自定义的线程池执行到了第1个任务
	22:28:46.337 pool-1-thread-2 自定义的线程池执行到了第2个任务
	22:28:46.338 pool-1-thread-2 自定义的线程池执行到了第4个任务
	22:28:46.338 pool-1-thread-1 自定义的线程池执行到了第3个任务
	22:28:46.339 pool-1-thread-2 自定义的线程池执行到了第5个任务
	22:28:46.339 pool-1-thread-1 自定义的线程池执行到了第6个任务
	22:28:46.339 pool-1-thread-2 自定义的线程池执行到了第7个任务
	22:28:46.339 pool-1-thread-1 自定义的线程池执行到了第8个任务
	22:28:46.340 pool-1-thread-2 自定义的线程池执行到了第9个任务
	22:28:46.340 pool-1-thread-1 自定义的线程池执行到了第10个任务

自定义的线程池通常仅保持最小量的线程数,只有短时间涌入大批任务的时候,才会把线程数加码到最大数量。

22.2 几种定时器线程池

个人总结概要:

参考文章

定时器线程池的工具类则叫做ScheduledExecutorService,添加了Scheduled前缀,表示它是一种有计划的、预先安排好的线程池。定时器线程池仅仅分成了两类:单线程的定时器线程池和固定数量的定时器线程池。

  1. 单线程的定时器线程池通过newSingleThreadScheduledExecutor方法获得
// 创建一个延迟一次的单线程定时器
ScheduledExecutorService pool = (ScheduledExecutorService) Executors.newSingleThreadScheduledExecutor();
  1. 固定数量的定时器线程池则通过newScheduledThreadPool方法获得
// 创建一个延迟一次的多线程定时器(线程池大小为3)
ScheduledExecutorService pool = (ScheduledExecutorService) Executors.newScheduledThreadPool(3);

定时器的调度方式:

线程池对象的方法 说明 参数说明
schedule 定时任务只启动一次 第一个参数为任务实例,第二个和第三个参数分别是延迟执行的时长及其单位
scheduleAtFixedRate 每间隔若干时间周期启动定时任务 第一个参数为任务实例,第二个参数为首次执行的延迟时长,第三个参数分别为后续运行的间隔时长,第四个参数则为时长单位
scheduleWithFixedDelay 固定延迟若干时间启动定时任务 参数说明基本同scheduleAtFixedRate方法

scheduleAtFixedRate和scheduleWithFixedDelay区别:前者的间隔时间从上个任务的开始时间起计算,后者的间隔时间从上个任务的结束时间起计算。

22.2 Fork+Join框架实现分而治之

个人总结概要:

	 1. Fork:增加线程(分);Join:减少线程(合)
	 2. 使用步骤:
	 	(1)定义递归任务类:继承RecursiveTask类(类似于实现Runnable接口),重写compute(类似于Run方法)
	 	(2)指定自定义线程池对象(递归任务拥有默认的内置线程池)并调用线程池对象的execute/invoke/submit三个方法之一启动递归任务。

参考文章

Java7新增了Fork/Join框架用以对症下药。该框架的Fork操作会按照树状结构不断分出下级线程,其对应的是分而治之的过程;而Join操作则把叶子线程的运算结果逐级合并,其对应的是汇聚归一的过程。

线程池对象的execute/invoke/submit三个方法:

方法 说明
execute 异步执行指定任务,且无返回值。
invoke 同步执行指定任务,并等待返回值,返回值就是最终的运算结果。
submit 异步执行指定任务,且返回结果任务对象。之后可择机调用结果任务的get方法获取最终的运算结果。
//定义一个求和的递归任务
public class SumTask extends RecursiveTask<Integer> {
	private static final long serialVersionUID = 1L;
	private static final int THRESHOLD = 20; // 不可再切割的元素个数门槛
	private int src[]; // 待求和的整型数组
	private int start; // 待求和的下标起始值
	private int end; // 待求和的下标终止值

	public SumTask(int[] src, int start, int end) {
		this.src = src;
		this.start = start;
		this.end = end;
	}

	// 对指定区间的数组元素求和
	private Integer subTotal() {
		Integer sum = 0;
		for (int i = start; i < end; i++) { // 求数组在指定区间的元素之和
			sum += src[i];
		}
		// 打印求和日志,包括当前线程的名称、起始数值、终止数值、区间之和
		String desc = String.format("%s ∑(%d~%d)=%d", Thread.currentThread().getName(), start, end, sum);
		System.out.println(desc);
		return sum;
	}

	@Override
	protected Integer compute() {
		if ((end - start) <= THRESHOLD) { // 不可再切割了
			return subTotal(); // 对指定区间的数组元素求和
		} else { // 区间过大,还能继续切割
			int middle = (start + end) / 2; // 计算区间中线的位置
			// 创建左边分区的求和任务
			SumTask left = new SumTask(src, start, middle);
			left.fork(); // 把左边求和任务添加到处理队列中
			// 创建右边分区的求和任务
			SumTask right = new SumTask(src, middle, end);
			right.fork(); // 把右边求和任务添加到处理队列中
			// 左边子任务的求和结果加上右边子任务的求和结果,等于当前任务的求和结果
			int sum = left.join() + right.join();
			// 打印求和日志,包括当前线程的名称、起始数值、终止数值、区间之和
			String desc = String.format("%s ∑(%d~%d)=%d", Thread.currentThread().getName(), start, end, sum);
			System.out.println(desc);
			return sum; // 返回本次任务的求和结果
		}
	}
}
// 测试任务以外的线程池框架
private static void testPoolTask() {
	// 下面初始化从0到99的整型数组
	int[] arr = new int[100];
	for (int i = 0; i < 100; i++) {
		arr[i] = i + 1;
	}
	// 创建一个求和的递归任务
	SumTask task = new SumTask(arr, 0, arr.length);
	// 创建一个用于分而治之的线程池,并发数量为6
	ForkJoinPool pool = new ForkJoinPool(6);
	// 命令线程池执行求和任务,并返回存放执行结果的任务对象
	ForkJoinTask<Integer> taskResult = pool.submit(task);
	try {
		Integer result = taskResult.get(); // 等待执行完成,并获取求和的结果数值
		System.out.println("最终计算结果: " + result);
	} catch (Exception e) {
		e.printStackTrace();
	}
	pool.shutdown(); // 关闭线程池
}

23、数据格式

23.1 URL地址的组成格式

参考文章

URL的全称是Uniform Resource Locator,意思是统一资源定位符,俗称网络地址或网址。
例如:http://www.news.cn:8080/Public/GetValidateCode?time=123#index
Java基础知识总结_第4张图片
井号之后的字串为引用位置,假设一个网页很长很长,打开后默认显示网页的顶部,造成用户下拉网页找到某块区域有点麻烦。而引用位置先给各区域做个编号,然后在URL末尾带上该位置的编号,于是网页打开后会自动滚到指定位置的区域,从而方便了用户的浏览操作。

URL工具常用的方法:

方法 说明
getProtocol 获取URL对象采用的网络协议。
getHost 获取URL对象的域名(主机名称)。
getDefaultPort 获取URL对象的默认端口。http协议的默认端口号是80,ftp协议的默认端口号是21,https协议的默认端口号是443。
getPort 获取URL对象的指定端口(若不显式指定则返回-1)。
getAuthority 获取URL对象的授权部门(由域名和指定端口组成)。
getPath 获取URL对象的路径(不包括域名)。
getQuery 获取URL对象的请求参数。
getFile 获取URL对象的文件名(由路径和请求参数组成)。
getRef 获取URL对象的引用位置。
openConnection 打开URL对象的网络连接,并返回URLConnection连接对象。无论是接口调用,还是上传下载,都依赖于这里的连接对象。

域名的合法性校验:

// 测试域名的可用信息。返回true表示域名合法,返回false表示域名非法
private static boolean testHost(String host) {
	try {
		// 根据域名或IP获得对应的网络地址对象
		InetAddress inet = InetAddress.getByName(host);
	} catch (UnknownHostException e) { // 如果host字符串并非合法的域名/IP,则getByName方法会扔出“未知的域名异常”
		e.printStackTrace();
		return false; // 返回false表示该字符串不是合法的域名/IP
	}
	return true; // 返回true表示该字符串是合法的域名/IP
}

常见字符对应的URL转义符:

Java基础知识总结_第5张图片

除了保留字符以外,中文字符一样需要转义,比如“你”要转为“%E4%BD%A0”。原始字符的转义过程也称作URL编码,反过来则有反转义过程,即将转义后的字符恢复为原始字符,反转义过程也称作URL解码。Java同时提供了对应的URL编码工具URLEncoder,以及URL解码工具URLDecoder,其中

URL编码的方法调用示例如下:

// 获得URL编码后的转义字符串
String encoded = URLEncoder.encode(origin);

URL解码的方法调用示例如下:

// 获得URL解码后的原始字符串
String origin = URLDecoder.decode(encoded);

23.2 JSON串的定义和解析

参考文章

处理JSON串工具,常见的json处理工具有阿里巴巴的FastJson
FastJson使用方法:

  1. 调用JSONObject的parseObject方法获得JSONObject对象
// 根据json串获得JSONObject对象
JSONObject object = JSONObject.parseObject(json);
  1. 调用JSONObject相关方法
方法 说明
getString 获取指定键名的字符串。
getIntValue 获取指定键名的整型数。
getDoubleValue 获取指定键名的双精度数。
getBooleanValue 获取指定键名的布尔值。
getJSONObject 获取指定键名的JSONObject对象。
getJSONArray 获取指定键名的JSONArray数组。注意JSONArray类型派生自清单List,意味着可以把它当作清单一样读写。
put 添加指定的键值对信息。
remove 移除指定键名的键值对。
clear 清空当前的JSONObject对象。
toJSONString 把JSONObject对象转换为字符串。
parseObject 获得json串对应的JSONObject对象
toJavaObject 将json串自动转换成实体对象
toJSONString 把实体对象转换成对应的json串

要求:json串跟对象类定义的数据结构一一对应,不管是参数名称还是参数类型全部吻合

// 根据json串获得JSONObject对象
JSONObject object = JSONObject.parseObject(json);
// 把JSONObject对象中的信息一一转成购物订单信息
GoodsOrder order = (GoodsOrder) JSONObject.toJavaObject(object, GoodsOrder.class);
// 把购物订单对象转换成json字符串
String json = JSONObject.toJSONString(order);

23.3 XML报文的定义和解析

参考文章

XML的全称是“Extensible Markup Language”(可扩展标记语言),它不但支持结构化数据的描述,还支持各类附加属性的定义,非常适合在网络中传输信息。
传统的XML解析方式有DOM和SAX两种,DOM方式会把整个XML报文读进来,并且所有节点全被自动加载到一个树状结构,以后每个节点值都到该树状结构中读取。SAX方式不会事先读入整个XML报文,而是根据节点名称从报文起点开始扫描,一旦找到该节点的标记头位置,即刻往后寻找该节点的标记尾,那么节点标记头尾之间的数据便是节点值了。DOM解析工具封装在包org.w3c.dom中,SAX解析工具封装在包javax.xml.parsers中,可是它俩用起来着实费劲,解析过程艰深晦涩,实际开发当中基本不予采用。

应用比较多的XML解析工具是第三方的Dom4j(遵循DOM规则),Dom4j解析XML报文的步骤:

  1. 创建SAXReader阅读器对象;
  2. 把字符串形式的XML报文转换为输入流对象;
  3. 命令阅读器对象从输入流中读取Document文档对象;
  4. 获得文档对象的根节点Element;
  5. 从根节点往下依次解析每个层级的节点值;

Element的相关方法:

方法 说明
getText 获得当前节点的字符串值。
element 获得当前节点下面指定名称的子节点对象。
elementText 获得当前节点下面指定名称的子节点值。
elements 获得当前节点下面指定名称的子节点清单。
attribute 获得当前节点自身指定名称的属性对象。
attributeValue 获得当前节点自身指定名称的属性值。
attributes 获得当前节点拥有的全部属性清单。

采用Dom4j解析该XML串的代码例子:

// 通过dom4j解析xml串
private static GoodsOrder testParserByDom4j(String xml) {
	GoodsOrder order = new GoodsOrder(); // 创建一个购物订单对象
	// 创建SAXReader阅读器对象
	SAXReader reader = new SAXReader();
	// 根据字符串构建字节数组输入流
	try (InputStream is = new ByteArrayInputStream(xml.getBytes(CHARSET))) {
		// 命令阅读器从输入流中读取文档对象
		Document document = reader.read(is);
		// 获得文档对象的根节点
		Element root = document.getRootElement();
		// 获取根节点下面名叫user_info的节点
		Element user_info = root.element("user_info");
		// 获取user_info节点下面名叫name的节点值
		order.user_info.name = user_info.element("name").getText();
		// 获取user_info节点下面名叫address的节点值
		order.user_info.address = user_info.element("address").getText();
		// 获取user_info节点下面名叫phone的节点值
		order.user_info.phone = user_info.element("phone").getText();
		System.out.println(String.format("用户信息如下:姓名=%s,地址=%s,手机号=%s", 
				order.user_info.name, order.user_info.address, order.user_info.phone));
		// 获取根节点下面名叫goods_list的节点清单
		List<Element> goods_list = root.element("goods_list").elements();
		for (int i=0; i<goods_list.size(); i++) { // 遍历商品节点清单
			Element goods_item = goods_list.get(i);
			GoodsItem item = new GoodsItem(); // 创建一项商品对象
			// 获取当前商品项节点下面名叫goods_name的节点值
			item.goods_name = goods_item.element("goods_name").getText();
			// 获取当前商品项节点下面名叫goods_number的节点值
			item.goods_number = Integer.parseInt(goods_item.element("goods_number").getText());
			// 获取当前商品项节点下面名叫goods_price的节点值
			item.goods_price = Double.parseDouble(goods_item.element("goods_price").getText());
			System.out.println(String.format("第%d个商品:名称=%s,数量=%d,价格=%f", 
					i+1, item.goods_name, item.goods_number, item.goods_price));
			order.goods_list.add(item); // 往商品清单中添加指定商品对象
		}
	} catch (Exception e) {
		e.printStackTrace();
	}
	return order; // 返回解析后的购物订单对象
}

除了解析各节点的节点值,Dom4j还能解析各节点的属性值。

24、HTTP访问

24.1 GET方式的HTTP调用

个人总结概要:
	
	 1. 网络访问步骤:
	 	(1)获取HTTP连接对象
	 	(2)展开连接对象的方法调用
	 	(3)从连接对象中获取字符编码及数据压缩类型,并进行处理(如果是压缩格式则解压)
	 	(4)把连接对象中输入流按照原字符编码格式转为字符串
	 	
	 2. GET方式除了支持从服务地址获取应答报文,还支持直接下载网络文件。

参考文章

计算机网络的通信标准主要采取TCP/IP协议组,该协议组又可分为三个层次:网络层、传输层和应用层。其中网络层包括IP协议、ICMP协议、ARP协议等等,传输层包括包含TCP协议与UDP协议,而应用层拥有FTP、HTTP、TELNET、SMTP等协议。
Java为HTTP编程提供的开发工具名叫HttpURLConnection获取HttpURLConnection实例的办法:调用URL对象的openConnection方法

URL url = new URL(address); // 根据网址字符串构建URL对象
// 打开URL对象的网络连接,并返回HttpURLConnection连接对象
HttpURLConnection conn = (HttpURLConnection) url.openConnection();

HttpURLConnection工具的几个基础方法:

方法 说明
setRequestMethod 设置连接对象的请求方式,主要有GET和POST两种。
setConnectTimeout 设置连接的超时时间,单位毫秒。
setReadTimeout 设置读取应答数据的超时时间,单位毫秒。
connect 开始连接,之后才能获取该网址返回的应答报文信息。
disconnect 断开连接。
getResponseCode 获取应答的状态码。200表示成功,403表示禁止访问,404表示页面不存在,500表示服务器内部错误。
getInputStream 获取连接的输入流对象,从输入流中可读出应答报文。
getContentLength 获取应答报文的长度。
getContentType 获取应答报文的类型。
getContentEncoding 获取应答报文的编码方式。

网络传输中把输入流中的数据转换为字符串:

//HTTP数据解析用到的工具类
public class StreamUtil {

	// 把输入流中的数据转换为字符串
	public static String isToString(InputStream is) throws IOException {
		byte[] bytes = new byte[is.available()]; // 创建临时存放的字节数组,available():获取流的长度,但是可能会出错,因为可能数据还未到达或者分批次到达
		is.read(bytes); // 从文件输入流中读取字节数组
		return  new String(bytes); // 把字节数组转换为字符串并返回
	}
}

展开连接对象的方法调用,以GET方式为例,按照顺序大致分为下列四个步骤:

  1. 设置各项请求参数,包括请求方式、连接超时、读取超时等等;
  2. 调用connect方法开启连接;
  3. 调用getInputStream方法得到输入流,并从中读出字符串形式的应答报文;
  4. 调用disconnect方法断开连接;

下面是指定网址发起GET调用,并获取应答报文的方法代码例子:

// 对指定url发起GET调用
private static void testCallGet(String callUrl) {
	try {
		URL url = new URL(callUrl); // 根据网址字符串构建URL对象
		// 打开URL对象的网络连接,并返回HttpURLConnection连接对象
		HttpURLConnection conn = (HttpURLConnection) url.openConnection();
		conn.setRequestMethod("GET"); // 设置请求方式为GET调用
		conn.setConnectTimeout(5000); // 设置连接的超时时间,单位毫秒
		conn.setReadTimeout(5000); // 设置读取应答数据的超时时间,单位毫秒
		conn.connect(); // 开始连接
		// 打印HTTP调用的应答内容长度、内容类型、内容编码
		System.out.println( String.format("应答内容长度=%d,内容类型=%s,编码方式=%s", 
				conn.getContentLength(), conn.getContentType(), conn.getContentEncoding()) );
		// 从输入流中获取默认的字符串数据,既不支持gzip解压,也不支持GBK编码
		String content = StreamUtil.isToString(conn.getInputStream());
		// 打印HTTP调用的应答状态码和应答报文
		System.out.println( String.format("应答状态码=%d,应答报文=%s", 
				conn.getResponseCode(), content) );
		conn.disconnect(); // 断开连接
	} catch (Exception e) {
		e.printStackTrace();
	}
}

HTTP接口调用

然后尝试通过上述的testCallGet方法获取实际业务信息,比如利用新浪财经的公开接口查询上证指数,调用代码如下所示:

testCallGet("https://hq.sinajs.cn/list=s_sh000001"); // 查询上证指数
应答内容长度=74, 内容类型=application/javascript; charset=GBK, 编码方式=null

应答状态码=200, 应答报文=var hq_str_s_sh000001="??????,3246.5714,30.2762,0.94,4691176,47515638";

返回报文出现了类似“???”的乱码?因为程序未能正确处理字符编码。目前的接口访问代码,默认采取国际通用的UTF-8编码,但中文世界有自己独立的一套GBK编码,股指接口返回的内容类型“application/javascript; charset=GBK”就表示本次返回的应答报文采取GBK编码。
有时为了提高传输效率,服务器会先压缩应答报文,再把压缩后的数据送给调用方,这样同样的信息只耗费较小的空间,从而降低了网络流量的占用。调用方先获取应答报文的压缩方式,如果发现服务器采用了gzip方式压缩数据,则调用方要对应答数据进行gzip解压;如果服务器未指定具体的压缩方式,则表示应答数据使用了默认的明文,调用方无需进行解压操作。

重新编写应答报文的获取方法,具体的方法代码示例如下:

// 把输入流中的数据按照指定字符编码转换为字符串
public static String isToString(InputStream is, String charset) throws IOException {
	byte[] bytes = new byte[is.available()]; // 创建临时存放的字节数组
	is.read(bytes); // 从文件输入流中读取字节数组
	return  new String(bytes, charset); // 把字节数组按照指定的字符编码转换为字符串并返回
}

// 从HTTP连接中获取已解压且重新编码后的应答报文
public static String getUnzipString(HttpURLConnection conn) throws IOException {
	// 获取应答报文的内容类型(包括字符编码)
	String contentType = conn.getContentType();
	String charset = "UTF-8"; // 默认的字符编码为UTF-8
	if (contentType != null) {
		if (contentType.toLowerCase().contains("charset=gbk")) { // 应答报文采用gbk编码
			charset = "GBK"; // 字符编码改为GBK
		} else if (contentType.toLowerCase().contains("charset=gb2312")) { // 应答报文采用gb2312编码
			charset = "GB2312"; // 字符编码改为GB2312
		}
	}
	// 获取应答报文的编码方式
	String contentEncoding = conn.getContentEncoding();
	// 获取HTTP连接的输入流对象
	InputStream is = conn.getInputStream();
	String result = "";
	if (contentEncoding != null && contentEncoding.contains("gzip")) { // 应答报文使用了gzip压缩
		// 根据输入流对象构建压缩输入流
		try (GZIPInputStream gis = new GZIPInputStream(is)) {
			// 把压缩输入流中的数据按照指定字符编码转换为字符串
			result = isToString(gis, charset);
		} catch (Exception e) {
			e.printStackTrace();
		}
	} else {
		// 把输入流中的数据按照指定字符编码转换为字符串
		result = isToString(is, charset);
	}
	return result; // 返回处理后的应答报文
}

GET方式除了支持从服务地址获取应答报文,还支持直接下载网络文件。二者的区别在于:应答报文是从连接对象的输入流中获取字符串,而文件下载要把输入流中的数据写入本地文件。下面是通过GET方式来下载网络文件的代码例子:

// 从指定url下载文件到本地
private static void testDownload(String filePath, String downloadUrl) {
	// 从下载地址中获取文件名
	String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));
	// 把本地目录与文件名拼接成为本地文件的完整路径
	String fullPath = filePath + "/" + fileName;
	// 根据指定路径构建文件输出流对象
	try (FileOutputStream fos = new FileOutputStream(fullPath)) {
		URL url = new URL(downloadUrl); // 根据网址字符串构建URL对象
		// 打开URL对象的网络连接,并返回HttpURLConnection连接对象
		HttpURLConnection conn = (HttpURLConnection) url.openConnection();
		conn.setRequestMethod("GET"); // 设置请求方式为GET调用
		conn.connect(); // 开始连接
		InputStream is = conn.getInputStream(); // 从连接对象中获取输入流
		// 以下把输入流中的数据写入本地文件
		byte[] data = new byte[1024];
		int len = 0;
		while((len = is.read(data)) > 0){
			fos.write(data, 0, len);
		}
		// 打印HTTP下载的文件大小、内容类型、编码方式
		System.out.println( String.format("文件大小=%dK, 内容类型=%s, 编码方式=%s", 
				conn.getContentLength()/1024, conn.getContentType(), conn.getContentEncoding()) );
		// 打印HTTP下载的应答状态码和文件保存路径
		System.out.println( String.format("应答状态码=%d, 文件保存路径=%s", 
				conn.getResponseCode(), fullPath) );
		conn.disconnect(); // 断开连接
	} catch (Exception e) {
		e.printStackTrace();
	}
}

24.2 POST方式的HTTP调用

参考文章

GET方式主要用于向服务器索取数据
POST方式服务器不仅仅作为信息提供方,还可以作为信息接收方,例如保存调用方提交的表单数据,或者保存调用方待上传的文件

POST方式的业务参数放在了请求报文当中。应答报文要从连接对象的输入流中获取,而请求报文要写入连接对象的输出流。

HttpURLConnection工具的几个方法补充(编码实现POST请求的时候):

方法 说明
setRequestMethod 要将请求方式设置为POST
setRequestProperty 设置请求属性。该方法可设置特定名称的属性值。
setDoOutput 准备让连接执行输出操作。默认为false(GET方式),POST方式需要设置为true。
setDoInput 准备让连接执行输入操作。默认为true,通常无需特意调用该方法。
getOutputStream 从连接对象中获取输出流,后续会把请求报文写入输出流。
getHeaderField 获取应答报文头部指定名称的字段值。该方法可得到特定名称的参数值,例如getHeaderField(“Content-Length”)返回的是应答报文的长度,getHeaderField(“Content-Type”)返回的是应答报文的内容类型,conn.getHeaderField(“Content-Encoding”)返回的是应答报文的编码方式。

setRequestProperty设置各式各样的属性值常见的属性名称及其属性值罗列如下:

属性名称 属性值
Content-Type 请求报文的内容类型。如果请求报文采取形如“参数A名称=A参数值&参数B名称=B参数值”的url参数格式,则内容类型应设置为“application/x-www-form-urlencoded”;如果请求报文是json格式,则内容类型应设置为“application/json”;如果请求报文是xml格式,则内容类型应设置为“application/xml”;如果请求报文是分段传输的文件数据,则内容类型应设置为“multipart/form-data;boundary=*”。
Connection 指定连接的保持方式。如果是文件上传,则必须设置为“Keep-Alive”,表示建议服务器保留连接,以便能够持续发送文件的分段数据。
User-Agent 指定调用方的浏览器类型。
Accept 指定可接受的应答报文类型。如果不设置则默认“/”,表示允许返回任何类型的应答报文;如果设置为“image/png”,则表示只接受返回png图片。
Accept-Language 指定可接受的应答报文语言。通常无需设置,如果只接受中文则可设置为“zh-cn”。
Accept-Encoding 指定可接受的应答报文编码方式。如果不设置则默认identity,表示不允许应答报文使用压缩;如果设置为gzip,则表示允许应答报文采用GZIP压缩,此时服务器可能返回gzip压缩的应答数据,也可能返回未压缩的应答数据。

请求报文是json串的HTTP接口例子,采用POST方式的调用方法代码如下所示:

// 对指定url发起POST调用
private static void testCallPost(String callUrl, String body) {
	try {
		URL url = new URL(callUrl); // 根据网址字符串构建URL对象
		// 打开URL对象的网络连接,并返回HttpURLConnection连接对象
		HttpURLConnection conn = (HttpURLConnection) url.openConnection();
		conn.setRequestMethod("POST"); // 设置请求方式为POST调用
		conn.setRequestProperty("Content-Type", "application/json"); // 请求报文为json格式
		conn.setDoOutput(true); // 准备让连接执行输出操作。默认为false,POST方式需要设置为true
		conn.connect(); // 开始连接
		OutputStream os = conn.getOutputStream(); // 从连接对象中获取输出流
		os.write(body.getBytes()); // 往输出流写入请求报文
		// 打印HTTP调用的应答内容长度、内容类型、编码方式
		System.out.println( String.format("应答内容长度=%s, 内容类型=%s, 编码方式=%s", 
				conn.getHeaderField("Content-Length"), conn.getHeaderField("Content-Type"),
				conn.getHeaderField("Content-Encoding")) );
		// 对输入流中的数据进行解压和字符编码,得到原始的应答字符串
		String content = StreamUtil.getUnzipString(conn);
		// 打印HTTP调用的应答状态码和应答报文
		System.out.println( String.format("应答状态码=%d, 应答报文=%s", 
				conn.getResponseCode(), content) );
		conn.disconnect(); // 断开连接
	} catch (Exception e) {
		e.printStackTrace();
	}
}
testCallPost("http://localhost:8080/NetServer/checkUpdate", "{\"package_list\":[{\"package_name\":\"com.qiyi.video\"}]}");

运行上述的POST代码,从以下的接口日志可知POST方式正确发送了请求报文,且正常收到了应答报文。

请求报文={"package_list":[{"package_name":"com.qiyi.video"}]}

应答内容长度=152, 内容类型=text/plain;charset=utf-8, 编码方式=null

应答状态码=200, 应答报文={"package_list":[{"package_name":"com.qiyi.video","download_url":"https://3g.lenovomm.com/w3g/yydownload/com.qiyi.video/60020","new_version":"10.2.0"}]}

通过HTTP接口上传文件也要采用POST方式,只是文件上传还需遵守一定的数据规则,除了内容类型设置为“multipart/form-data;boundary=***”(***处要替换成边界字符串),请求报文也得依顺序填入报文头、报文体和报文尾,详细的上传过程代码如下所示:

// 把本地文件上传给指定url
private static void testUpload(String filePath, String uploadUrl) {
	// 从本地文件路径获取文件名
	String fileName = filePath.substring(filePath.lastIndexOf("/"));
	String end = "\r\n"; // 结束字符串
	String hyphens = "--"; // 连接字符串
	String boundary = "WUm4580jbtwfJhNp7zi1djFEO3wNNm"; // 边界字符串
	try (FileInputStream fis = new FileInputStream(filePath)) {
		URL url = new URL(uploadUrl); // 根据网址字符串构建URL对象
		// 打开URL对象的网络连接,并返回HttpURLConnection连接对象
		HttpURLConnection conn = (HttpURLConnection) url.openConnection();
		conn.setDoOutput(true); // 准备让连接执行输出操作。默认为false,POST方式都要设置为true
		conn.setRequestMethod("POST"); // 设置请求方式为POST调用
		// 连接过程要保持活跃
		conn.setRequestProperty("Connection", "Keep-Alive");
		// 请求报文要求分段传输,并且各段之间以边界字符串隔开
		conn.setRequestProperty("Content-Type", "multipart/form-data;boundary=" + boundary);
		// 根据连接对象的输出流构建数据输出流
		DataOutputStream ds = new DataOutputStream(conn.getOutputStream());
		// 以下写入请求报文的头部
		ds.writeBytes(hyphens + boundary + end);
		ds.writeBytes("Content-Disposition: form-data; "
				+ "name=\"file\";filename=\"" + fileName + "\"" + end);
		ds.writeBytes(end);
		// 以下写入请求报文的主体
		byte[] buffer = new byte[1024];
		int length;
		// 先将文件数据写入到缓冲区,再将缓冲数据写入输出流
		while ((length = fis.read(buffer)) != -1) {
			ds.write(buffer, 0, length);
		}
		ds.writeBytes(end);
		// 以下写入请求报文的尾部
		ds.writeBytes(hyphens + boundary + hyphens + end);
		ds.close(); // 关闭数据输出流
		// 对输入流中的数据进行解压和字符编码,得到原始的应答字符串
		String content = StreamUtil.getUnzipString(conn);
		// 打印HTTP上传的应答状态码和应答报文
		System.out.println( String.format("应答状态码=%d, 应答报文=%s", 
				conn.getResponseCode(), content) );
		conn.disconnect(); // 断开连接
	} catch (Exception e) {
		e.printStackTrace();
	}
}

然后由外部在调用testUpload方法时输入上传地址和待上传的文件路径,具体代码示例如下:

testUpload("E:/bliss.jpg", "http://localhost/NetServer/uploadServlet");

24.3 Java11新增的HttpClient

参考文章

24.4 HttpClient实现下载与上传

参考文章

25、Socket通信

个人总结概述:
	TCP通信:
	 1. ServerSocket使用步骤:
		(1)定义端口号
		(2)创建服务端套接字serverSocket ,监听客户端Socket的连接请求
		(3)while循环监听客户端连接请求,使用serverSocket的accept()方法连接客户端(返回客户端socket)
		(4)开启新线程,使用返回的客户端socket的相关方法进行业务处理
		
	 2. Socket使用步骤:
	 	(1)定义服务端的IP地址和端口号
	 	(2)创建套接字对象socket
	 	(3)创建输入流对象
	 	(4)根据服务端IP和端口,使用SocketAddress接口的实现类SocketAddress构造SocketAddress对象,获取IP地址的封装对象socketAddress 
	 	(5)使用socket的connect(socketAddress ,3000)方法连接服务端
	 	(6)使用套接字对象socket的getOutputStream()方法获取socket的输出流对象writer
	 	(7)通过输出流对象writer的write方法,往Socket连接中写入数据,发送数据到服务端
	 	(8)关闭连接
	
	UDP通信:
	 1. 服务端:
	 (1)	定义端口
	 (2)创建datagramSocket 对象
	 (3)创建datagramPacket对象
	 (4)while持续监听,datagramSocket 的receive(datagramPacket)方法接收数据包
	 (5)调用datagramPacket方法对接收的数据进行相关处理
	 2. 客户端:
	 (1)定义服务端的IP地址和端口号
	 (2)创建datagramSocket对象
	 (3)根据IP地址使用InetAddress的getByName方法获得对应的服务端网络地址对象serverAddress
	 (4)构造datagramPacket对象,传入传输的数据,长度,延迟,服务器地址,端口号
	 (5)调用datagramSocket 的send方法,向服务器发送数据包datagramPacket

25.1 利用Socket传输文本消息

参考文章

HTTP协议的缺陷:

  1. HTTP连接属于短连接,每次访问操作结束之后,客户端便会关闭本次连接。下次还想访问接口的话,就得重新建立连接,要是频繁发生数据交互的话,反复的连接和断开将造成大量的资源消耗。
  2. 在HTTP连接中,服务端总是被动接收消息,无法主动向客户端推送消息。倘若客户端不去请求服务端,服务端就没法发送即时消息。
  3. 每次HTTP调用都属于客户端与服务端之间的一对一交互,完全与第三者无关(比如另一个客户端),这种技术手段无法满足类似QQ聊天那种群发消息的要求。
  4. HTTP连接需要搭建专门的HTTP服务器,这样的服务端比较重,不适合两个设备终端之间的简单信息传输。

在Java编程中,网络通信的基本操作单元其实是套接字Socket,它本身不是什么协议,而是一种支持TCP/IP协议的通信接口。

  • TCP协议:Socket连接的双方握手确认连上
  • UDP协议:Socket连接的双方未确认连上就自顾自地发送数据

Socket工具虽然主要用于客户端,但服务端通常也保留一份客户端的Socket备份,它描述了两边对套接字处理的一般行为。Socket类的主要方法说明:

方法 说明
connect 连接指定IP和端口。该方法用于客户端连接服务端,成功连上之后才能开展数据交互。
getInputStream 获取套接字的输入流,输入流用于接收对方发来的数据。
getOutputStream 获取套接字的输出流,输出流用于向对方发送数据。
isConnected 判断套接字是否连上。
close 关闭套接字。套接字关闭之后将无法再传输数据。
isClosed 判断套接字是否关闭。

ServerSocket仅用于服务端,它的构造函数可指定侦听指定端口,从而及时响应客户端的连接请求。下面是ServerSocket的主要方法说明:

方法 说明
accept 开始接收客户端的连接。一旦有客户端连上,就返回该客户端的套接字对象。若要持续侦听连接,得在循环语句中调用该方法。
close 关闭服务端的套接字。
isClosed 判断服务端的套接字是否关闭。

套接字的客户端需要给每个连接分配两个线程,其中一个线程专门用来向服务端发送信息,而另一个线程专门用于从服务端接收信息。

25.2 使用Socket开展文件传输

参考文章

25.3 采用UDP协议的Socket通信

参考文章

UDP协议,Java给出的实现工具包括数据包套接字DatagramSocket和数据包裹DatagramPacket。
DatagramSocket提供了设备间的数据交互动作,它的主要方法说明如下:

方法 说明
构造方法 对于服务端来说,构造方法需要指定待侦听的端口号;对于客户端来说,构造方法无需任何参数。
receive 该方法用于服务端接收数据。
send 该方法用于客户端发送数据。
close 关闭数据包套接字。

注意:上面的receive和send两个方法,它们的输入参数类型为DatagramPacket,也就是说,必须先将数据封装为DatagramPacket格式,才能在UDP的服务端与客户端之间传输。
DatagramPacket的主要方法说明:

方法 说明
用于服务端的构造方法 此时构造方法只有两个参数,分别为字节数组及其长度。
用于客户端的构造方法 此时构造方法拥有四个参数,依次为字节数组、数组长度、数据要发往的服务器InetAddress地址、服务器的端口号。
getData 获取数据包裹里的字节数组。
getOffset 获取数据的起始偏移。
getLength 获取数据的长度。

26、JDBC编程

26.1 JDBC的应用原理

参考文章

需要导入数据库驱动jar包,mysql-connector-java-8.0.16.jar

JDBC四要素:

  1. 数据库的驱动(com.mysql.cj.jdbc.Driver)
  2. 数据库的连接地址(jdbc:mysql://IP地址:端口号/数据库名称,注意新版的MySQL还需在地址后面补充时区信息,否则运行会报错。下面是一个完整的MySQL连接地址例子:
    jdbc:mysql://localhost:3306/study?serverTimezone=GMT%2B8)
  3. 数据库的用户名
  4. 数据库的密码

Connection的几个方法:

方法 说明
close 关闭数据库连接
isClosed 获取数据库的连接状态,返回true表示连接已关闭,返回false表示连接未关闭。
getCatalog 获取该连接的数据库实例名称。
getAutoCommit 获取数据库的自动提交标志。如果该标志设置为true,则每次执行一条SQL语句,系统都会自动提交该语句的修改内容。
setAutoCommit 设置自动提交的标志,默认为true表示自动提交。
commit 提交数据库的修改。
rollback 回滚数据库的修改。注意要先关闭自动提交,才能通过rollback方法回滚事务。否则报错“Can’t call rollback when autocommit=true”。
createStatement 创建数据库操作的执行报告。
prepareStatement 创建数据库操作的预备报告。

26.2 通过JDBC管理数据库

参考文章1
参考文章2

Statement对象由Connection的createStatement方法获得,它主要提供了下列两个方法:

方法 说明
executeUpdate 执行数据库的管理语句,主要包含建表、改表结构、删表、增加记录、修改记录、删除记录等等。它的返回值是整型,存放着当前语句的操作记录数量,例如删除了多少条记录、更新了多少条记录等。
executeQuery 执行数据库的查询语句,专用于select命令。它的返回值是ResultSet类型,查询的结果集可经由ResultSet对象得到。

ResultSet的常见方法分成三类,说明如下:

方法 说明
1、移动游标 该类方法可将当前游标移动到指定位置,主要包括下列方法:
next 将游标移到后一条记录。该方法返回true表示尚未移到末尾,返回false表示已经移到末尾。
absolute 将游标移到第几条记录,如果参数为负数则表示倒数的第几条。
first 将游标移到第一条记录。
last 将游标移到最后一条记录。
previous 将游标移到前一条记录。
beforeFirst 将游标移到第一条记录之前。
afterLast 将游标移到最后一条记录之后。
2、判断游标位置 该类方法可判断当前游标是否处于某个位置,主要包括下列方法:
isFirst 游标是否指向第一条记录。
isLast 游标是否指向最后一条记录。
isBeforeFirst 游标是否在第一条记录之前。
isAfterLast 游标是否在最后一条记录之后。
3、从当前游标获取数据 该类方法可从当前游标指向的记录中获取字段值,当方法参数为整型时,表示获取指定序号的字段值;当方法参数为字符串时,表示获取指定名称的字段值。相关的获取方法罗列如下:
getInt 获取指定序号或者指定名称的字段整型值。
getLong 获取指定序号或者指定名称的字段长整值。
getFloat 获取指定序号或者指定名称的字段浮点值。
getDouble 获取指定序号或者指定名称的字段双精度值。
getString 获取指定序号或者指定名称的字段字符串值。
getDate 获取指定序号或者指定名称的字段日期值。

Statement对象操作SQL步骤:

  1. 获取数据库连接connection :调用DriverManager类的getConnection方法获得连接对象。
  2. 创建该连接的执行报告statement:调用Connection对象的createStatement方法获得Statement。
  3. 命令报告执行SQL语句:调用statement的executeUpdate或executeQuery方法来执行SQL语句。

注意:MySQL未提供将日期转成字符串的to_char函数,因而只能先取到Date类型的字段值,再将其通过Java代码转为字符串
日期类型转换为字符串类型的方法代码如下所示:

// 获取指定格式的日期字符串
public static String getFormatDate(Date date) {
	// 创建一个日期格式化的工具
	SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
	// 将当前日期时间按照指定格式输出格式化后的日期时间字符串
	return sdf.format(date);
}

26.3 引入预报告,防止SQL注入

参考文章

预报告定义了新类PreparedStatement,与原报告Statement不同的是,创建预报告对象时就要设定SQL语句,并且SQL里面的动态参数以问号代替。然后准备调用executeUpdate或者executeQuery之前,先调用预报告对象的setString方法来设置对应序号的参数值。预报告会把字符串中的单引号做转义。

———————————————————————————

JVM相关

1、eclipse开发java内存设置

使用eclipse开发Java的话,默认的堆内存和栈内存大小在eclipse.ini里面设置,ini文件中的Xmx参数表示JVM最大的堆内存大小,该参数通常配置为512M;另一个Xss参数表示每个线程的栈内存大小,该参数默认为1M。所以,一旦程序意图占用超过512M的内存空间,就会报堆内存溢出的错误;一旦某次方法调用需要传送超过1M大小的参数信息,就会报栈溢出的错误。

你可能感兴趣的:(Java笔记)