本系列文章经补充和完善,已修订整理成书《Java编程的逻辑》,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接http://item.jd.com/12299018.html


本节我们介绍在Java中如何以二进制字节的方式来处理文件,上节我们提到Java中有流的概念,以二进制方式读写的主要流有:

  • InputStream/OutputStream: 这是基类,它们是抽象类。
  • FileInputStream/FileOutputStream: 输入源和输出目标是文件的流。
  • ByteArrayInputStream/ByteArrayOutputStream: 输入源和输出目标是字节数组的流。
  • DataInputStream/DataOutputStream: 装饰类,按基本类型和字符串而非只是字节读写流。
  • BufferedInputStream/BufferedOutputStream: 装饰类,对输入输出流提供缓冲功能。

下面,我们就来介绍这些类的功能、用法、原理和使用场景,最后,我们总结一些简单的实用方法。

InputStream/OutputStream

InputStream的基本方法

InputStream是抽象类,主要方法是:

public abstract int read() throws IOException;

read从流中读取下一个字节,返回类型为int,但取值在0到255之间,当读到流结尾的时候,返回值为-1,如果流中没有数据,read方法会阻塞直到数据到来、流关闭、或异常出现,异常出现时,read方法抛出异常,类型为IOException,这是一个受检异常,调用者必须进行处理。read是一个抽象方法,具体子类必须实现,FileInputStream会调用本地方法,所谓本地方法,一般不是用Java写的,大多使用C语言实现,具体实现往往与虚拟机和操作系统有关。

InputStream还有如下方法,可以一次读取多个字节:

public int read(byte b[]) throws IOException

读入的字节放入参数数组b中,第一个字节存入b[0],第二个存入b[1],以此类推,一次最多读入的字节个数为数组b的长度,但实际读入的个数可能小于数组长度,返回值为实际读入的字节个数。如果刚开始读取时已到流结尾,则返回-1,否则,只要数组长度大于0,该方法都会尽力至少读取一个字节,如果流中一个字节都没有,它会阻塞,异常出现时也是抛出IOException。该方法不是抽象方法,InputStream有一个默认实现,主要就是循环调用读一个字节的read方法,但子类如FileInputStream往往会提供更为高效的实现。

批量读取还有一个更为通用的重载方法:

public int read(byte b[], int off, int len) throws IOException

读入的第一个字节放入b[off],最多读取len个字节,read(byte b[])就是调用了该方法:

public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}

流读取结束后,应该关闭,以释放相关资源,关闭方法为:

public void close() throws IOException

不管read方法是否抛出了异常,都应该调用close方法,所以close通常应该放在finally语句内。close自己可能也会抛出IOException,但通常可以捕获并忽略。

InputStream的高级方法

InputStream还定义了如下方法:

public long skip(long n) throws IOException
public int available() throws IOException
public synchronized void mark(int readlimit)
public boolean markSupported()
public synchronized void reset() throws IOException

skip跳过输入流中n个字节,因为输入流中剩余的字节个数可能不到n,所以返回值为实际略过的字节个数。InputStream的默认实现就是尽力读取n个字节并扔掉,子类往往会提供更为高效的实现,FileInputStream会调用本地方法。在处理数据时,对于不感兴趣的部分,skip往往比读取然后扔掉的效率要高。

available返回下一次不需要阻塞就能读取到的大概字节个数。InputStream的默认实现是返回0,子类会根据具体情况返回适当的值,FileInputStream会调用本地方法。在文件读写中,这个方法一般没什么用,但在从网络读取数据时,可以根据该方法的返回值在网络有足够数据时才读,以避免阻塞。

一般的流读取都是一次性的,且只能往前读,不能往后读,但有时可能希望能够先看一下后面的内容,根据情况,再重新读取。比如,处理一个未知的二进制文件,我们不确定它的类型,但可能可以通过流的前几十个字节判断出来,判读出来后,再重置到流开头,交给相应类型的代码进行处理。

InputStream定义了三个方法,mark/reset/markSupported,用于支持从读过的流中重复读取。怎么重复读取呢?先使用mark方法将当前位置标记下来,在读取了一些字节,希望重新从标记位置读时,调用reset方法。

能够重复读取不代表能够回到任意的标记位置,mark方法有一个参数readLimit,表示在设置了标记后,能够继续往后读的最多字节数,如果超过了,标记会无效。为什么会这样呢?因为之所以能够重读,是因为流能够将从标记位置开始的字节保存起来,而保存消耗的内存不能无限大,流只保证不会小于readLimit。

不是所有流都支持mark/reset的,是否支持可以通过markSupported的返回值进行判断。InpuStream的默认实现是不支持,FileInputStream也不直接支持,但BufferedInputStream和ByteArrayInputStream可以。

OutputStream

OutputStream的基本方法是:

public abstract void write(int b) throws IOException;

向流中写入一个字节,参数类型虽然是int,但其实只会用到最低的8位。这个方法是抽象方法,具体子类必须实现,FileInputStream会调用本地方法。

OutputStream还有两个批量写入的方法:

public void write(byte b[]) throws IOException
public void write(byte b[], int off, int len) throws IOException

在第二个方法中,第一个写入的字节是b[off],写入个数为len,最后一个是b[off+len-1],第一个方法等同于调用:write(b, 0, b.length);。OutputStream的默认实现是循环调用单字节的write方法,子类往往有更为高效的实现,FileOutpuStream会调用对应的批量写本地方法。

OutputStream还有两个方法:

public void flush() throws IOException
public void close() throws IOException

flush将缓冲而未实际写的数据进行实际写入,比如,在BufferedOutputStream中,调用flush会将其缓冲区的内容写到其装饰的流中,并调用该流的flush方法。基类OutputStream没有缓冲,flush代码为空。

需要说明的是文件输出流FileOutputStream,你可能会认为,调用flush会强制确保数据保存到硬盘上,但实际上不是这样,FileOutputStream没有缓冲,没有重写flush,调用flush没有任何效果,数据只是传递给了操作系统,但操作系统什么时候保存到硬盘上,这是不一定的。要确保数据保存到了硬盘上,可以调用FileOutputStream中的特有方法。

close一般会首先调用flush,然后再释放流占用的系统资源。同InputStream一样,close一般应该放在finally语句内。

FileInputStream/FileOutputStream

FileOutputStream

FileOutputStream的主要构造方法有:

public FileOutputStream(File file) throws FileNotFoundException
public FileOutputStream(File file, boolean append) throws FileNotFoundException
public FileOutputStream(String name) throws FileNotFoundException
public FileOutputStream(String name, boolean append) throws FileNotFoundException

有两类参数,一类是文件路径,可以是File对象file,也可以是文件路径name,路径可以是绝对路径,也可以是相对路径,如果文件已存在,append参数指定是追加还是覆盖,true表示追加,没传append参数表示覆盖。new一个FileOutputStream对象会实际打开文件,操作系统会分配相关资源。如果当前用户没有写权限,会抛出异常SecurityException,它是一种RuntimeException。如果指定的文件是一个已存在的目录,或者由于其他原因不能打开文件,会抛出异常FileNotFoundException,它是IOException的一个子类。

我们看一段简单的代码,将字符串"hello, 123, 老马"写到文件hello.txt中:

OutputStream output =  new FileOutputStream("hello.txt");
try{
String data = "hello, 123, 老马";
byte[] bytes = data.getBytes(Charset.forName("UTF-8"));
output.write(bytes);
}finally{
output.close();
}

OutputStream只能以byte或byte数组写文件,为了写字符串,我们调用String的getBytes方法得到它的UTF-8编码的字节数组,再调用write方法,写的过程放在try语句内,在finally语句中调用close方法。

FileOutputStream还有两个额外的方法:

public FileChannel getChannel()
public final FileDescriptor getFD()

FileChannel定义在java.nio中,表示文件通道概念,我们不会深入介绍通道,但内存映射文件方法定义在FileChannel中,我们会在后续章节介绍。FileDescriptor表示文件描述符,它与操作系统的一些文件内存结构相连,在大部分情况下,我们不会用到它,不过它有一个方法sync:

public native void sync() throws SyncFailedException;

这是一个本地方法,它会确保将操作系统缓冲的数据写到硬盘上。注意与OutputStream的flush方法相区别,flush只能将应用程序缓冲的数据写到操作系统,sync则确保数据写到硬盘,不过一般情况下,我们并不需要手工调用它,只要操作系统和硬件设备没问题,数据迟早会写入,但在一定特定情况下,一定需要确保数据写入硬盘,则可以调用该方法。

FileInputStream

FileInputStream的主要构造方法有:

public FileInputStream(String name) throws FileNotFoundException
public FileInputStream(File file) throws FileNotFoundException

参数与FileOutputStream类似,可以是文件路径或File对象,但必须是一个已存在的文件,不能是目录。new一个FileInputStream对象也会实际打开文件,操作系统会分配相关资源,如果文件不存在,会抛出异常FileNotFoundException,如果当前用户没有读的权限,会抛出异常SecurityException。

我们看一段简单的代码,将上面写入的文件"hello.txt"读到内存并输出:

InputStream input = new FileInputStream("hello.txt");
try{
byte[] buf = new byte[1024];
int bytesRead = input.read(buf);
String data = new String(buf, 0, bytesRead, "UTF-8");
System.out.println(data);
}finally{
input.close();
}

读入到的是byte数组,我们使用String的带编码参数的构造方法将其转换为了String。这段代码假定一次read调用就读到了所有内容,且假定字节长度不超过1024。为了确保读到所有内容,可以逐个字节读取直到文件结束:

int b = -1;
int bytesRead = 0;
while((b=input.read())!=-1){
buf[bytesRead++] = (byte)b;
}

在没有缓冲的情况下逐个字节读取性能很低,可以使用批量读入且确保读到文件结尾,如下所示:

byte[] buf = new byte[1024];
int off = 0;
int bytesRead = 0;
while((bytesRead=input.read(buf, off, 1024-off ))!=-1){
off += bytesRead;
}
String data = new String(buf, 0, off, "UTF-8");

不过,这还是假定文件内容长度不超过一个固定的大小1024。如果不确定文件内容的长度,不希望一次性分配过大的byte数组,又希望将文件内容全部读入,怎么做呢?可以借助ByteArrayOutputStream。

ByteArrayInputStream/ByteArrayOutputStream

ByteArrayOutputStream

ByteArrayOutputStream的输出目标是一个byte数组,这个数组的长度是根据数据内容动态扩展的。它有两个构造方法:

public ByteArrayOutputStream()
public ByteArrayOutputStream(int size)

第二个构造方法中的size指定的就是初始的数组大小,如果没有指定,长度为32。在调用write方法的过程中,如果数组大小不够,会进行扩展,扩展策略同样是指数扩展,每次至少增加一倍。

ByteArrayOutputStream有如下方法,可以方便的将数据转换为字节数组或字符串:

public synchronized byte[] toByteArray()
public synchronized String toString()
public synchronized String toString(String charsetName)

toString()方法使用系统默认编码。

ByteArrayOutputStream中的数据也可以方便的写到另一个OutputStream:

public synchronized void writeTo(OutputStream out) throws IOException

ByteArrayOutputStream还有如下额外方法:

public synchronized int size()
public synchronized void reset()

size返回当前写入的字节个数。reset重置字节个数为0,reset后,可以重用已分配的数组。

使用ByteArrayOutputStream,我们可以改进上面的读文件代码,确保将所有文件内容读入:

InputStream input = new FileInputStream("hello.txt");
try{
ByteArrayOutputStream output = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
int bytesRead = 0;
while((bytesRead=input.read(buf))!=-1){
output.write(buf, 0, bytesRead);
}
String data = output.toString("UTF-8");
System.out.println(data);
}finally{
input.close();
}

读入的数据先写入ByteArrayOutputStream中,读完后,再调用其toString方法获取完整数据。

ByteArrayInputStream

ByteArrayInputStream将byte数组包装为一个输入流,是一种适配器模式,它的构造方法有:

public ByteArrayInputStream(byte buf[])
public ByteArrayInputStream(byte buf[], int offset, int length)

第二个构造方法以buf中offset开始length个字节为背后的数据。ByteArrayInputStream的所有数据都在内存,支持mark/reset重复读取。

为什么要将byte数组转换为InputStream呢?这与容器类中要将数组、单个元素转换为容器接口的原因是类似的,有很多代码是以InputStream/OutputSteam为参数构建的,它们构成了一个协作体系,将byte数组转换为InputStream可以方便的参与这种体系,复用代码。

DataInputStream/DataOutputStream

上面介绍的类都只能以字节为单位读写,如何以其他类型读写呢?比如int, double。可以使用DataInputStream/DataOutputStream,它们都是装饰类。

DataOutputStream

DataOutputStream是装饰类基类FilterOutputStream的子类,FilterOutputStream是OutputStream的子类,它的构造方法是:

public FilterOutputStream(OutputStream out)

它接受一个已有的OutputStream,基本上将所有操作都代理给了它。

DataOutputStream实现了DataOutput接口,可以以各种基本类型和字符串写入数据,部分方法如下:

void writeBoolean(boolean v) throws IOException;
void writeInt(int v) throws IOException;
void writeDouble(double v) throws IOException;
void writeUTF(String s) throws IOException;

在写入时,DataOutputStream会将这些类型的数据转换为其对应的二进制字节,比如:

  • writeBoolean: 写入一个字节,如果值为true,则写入1,否则0
  • writeInt: 写入四个字节,最高位字节先写入,最低位最后写入
  • writeUTF: 将字符串的UTF-8编码字节写入,这个编码格式与标准的UTF-8编码略有不同,不过,我们不用关心这个细节。

与FilterOutputStream一样,DataOutputStream的构造方法也是接受一个已有的OutputStream:

public DataOutputStream(OutputStream out)

我们来看一个例子,保存一个学生列表到文件中,学生类的定义为:

class Student {
String name;
int age;
double score; public Student(String name, int age, double score) {
...
}
...
}

我们省略了构造方法和getter/setter方法,学生列表内容为:

List<Student> students = Arrays.asList(new Student[]{
new Student("张三", 18, 80.9d),
new Student("李四", 17, 67.5d)
});

将该列表内容写到文件students.dat中的代码可以为:

public static void writeStudents(List<Student> students) throws IOException{
DataOutputStream output = new DataOutputStream(
new FileOutputStream("students.dat"));
try{
output.writeInt(students.size());
for(Student s : students){
output.writeUTF(s.getName());
output.writeInt(s.getAge());
output.writeDouble(s.getScore());
}
}finally{
output.close();
}
}

我们先写了列表的长度,然后针对每个学生、每个字段,根据其类型调用了相应的write方法。

DataInputStream

DataInputStream是装饰类基类FilterInputStream的子类,FilterInputStream是InputStream的子类。

DataInputStream实现了DataInput接口,可以以各种基本类型和字符串读取数据,部分方法如下:

boolean readBoolean() throws IOException;
int readInt() throws IOException;
double readDouble() throws IOException;
String readUTF() throws IOException;

在读取时,DataInputStream会先按字节读进来,然后转换为对应的类型。

DataInputStream的构造方法接受一个InputStream:

public DataInputStream(InputStream in)

还是以上面的学生列表为例,我们来看怎么从文件中读进来:

public static List<Student> readStudents() throws IOException{
DataInputStream input = new DataInputStream(
new FileInputStream("students.dat"));
try{
int size = input.readInt();
List<Student> students = new ArrayList<Student>(size);
for(int i=0; i<size; i++){
Student s = new Student();
s.setName(input.readUTF());
s.setAge(input.readInt());
s.setScore(input.readDouble());
students.add(s);
}
return students;
}finally{
input.close();
}
}

基本是写的逆过程,代码比较简单,就不赘述了。

使用DataInputStream/DataOutputStream读写对象,非常灵活,但比较麻烦,所以Java提供了序列化机制,我们在后续章节介绍。

BufferedInputStream/BufferedOutputStream

FileInputStream/FileOutputStream是没有缓冲的,按单个字节读写时性能比较低,虽然可以按字节数组读取以提高性能,但有时必须要按字节读写,比如上面的DataInputStream/DataOutputStream,它们包装了文件流,内部会调用文件流的单字节读写方法。怎么解决这个问题呢?方法是将文件流包装到缓冲流中。

BufferedInputStream内部有个字节数组作为缓冲区,读取时,先从这个缓冲区读,缓冲区读完了再调用包装的流读,它的构造方法有两个:

public BufferedInputStream(InputStream in)
public BufferedInputStream(InputStream in, int size)

size表示缓冲区大小,如果没有,默认值为8192。

除了提高性能,BufferedInputStream也支持mark/reset,可以重复读取。

与BufferedInputStream类似,BufferedOutputStream的构造方法也有两个,默认的缓冲区大小也是8192,它的flush方法会将缓冲区的内容写到包装的流中。

在使用FileInputStream/FileOutputStream时,应该几乎总是在它的外面包上对应的缓冲类,如下所示:

InputStream input = new BufferedInputStream(new FileInputStream("hello.txt"));
OutputStream output = new BufferedOutputStream(new FileOutputStream("hello.txt"));

再比如:

DataOutputStream output = new DataOutputStream(
new BufferedOutputStream(new FileOutputStream("students.dat")));
DataInputStream input = new DataInputStream(
new BufferedInputStream(new FileInputStream("students.dat")));

实用方法

可以看出,即使只是按二进制字节读写流,Java也包括了很多的类,虽然很灵活,但对于一些简单的需求,却需要写很多代码,实际开发中,经常需要将一些常用功能进行封装,提供更为简单的接口。下面我们提供一些实用方法,以供参考。

拷贝

拷贝输入流的内容到输出流,代码为:

public static void copy(InputStream input,
OutputStream output) throws IOException{
byte[] buf = new byte[4096];
int bytesRead = 0;
while((bytesRead = input.read(buf))!=-1){
output.write(buf, 0, bytesRead);
}
}

将文件读入字节数组

代码为:

public static byte[] readFileToByteArray(String fileName) throws IOException{
InputStream input = new FileInputStream(fileName);
ByteArrayOutputStream output = new ByteArrayOutputStream();
try{
copy(input, output);
return output.toByteArray();
}finally{
input.close();
}
}

这个方法调用了上面的拷贝方法。

将字节数组写到文件

public static void writeByteArrayToFile(String fileName,
byte[] data) throws IOException{
OutputStream output = new FileOutputStream(fileName);
try{
output.write(data);
}finally{
output.close();
}
}

Apache有一个类库Commons IO,里面提供了很多简单易用的方法,实际开发中,可以考虑使用。

小结

本节我们介绍了如何在Java中以二进制字节的方式读写文件,介绍了主要的流。

  • InputStream/OutputStream:是抽象基类,有很多面向流的代码,以它们为参数,比如本节介绍的copy方法。
  • FileInputStream/FileOutputStream:流的源和目的地是文件。
  • ByteArrayInputStream/ByteArrayOutputStream:源和目的地是字节数组,作为输入相当于是适配器,作为输出封装了动态数组,便于使用。
  • DataInputStream/DataOutputStream:装饰类,按基本类型和字符串读写流。
  • BufferedInputStream/BufferedOutputStream:装饰类,提供缓冲,FileInputStream/FileOutputStream一般总是应该用该类装饰。

最后,我们提供了一些实用方法,以方便常见的操作,在实际开发中,可以考虑使用专门的类库如Apache Commons IO。

本节介绍的流不适用于处理文本文件,比如,不能按行处理,没有编码的概念,下一节,就让我们来看文本文件和字符流。

----------------

未完待续,查看最新文章,敬请关注微信公众号“老马说编程”(扫描下方二维码),从入门到高级,深入浅出,老马和你一起探索Java编程及计算机技术的本质。用心原创,保留所有版权。

Java编程的逻辑 (57) - 二进制文件和字节流的更多相关文章

  1. 《Java编程的逻辑》 - 文章列表

    <计算机程序的思维逻辑>系列文章已整理成书<Java编程的逻辑>,由机械工业出版社出版,2018年1月上市,各大网店有售,敬请关注! 京东自营链接:https://item.j ...

  2. Java编程的逻辑 (58) - 文本文件和字符流

    本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...

  3. Java编程的逻辑 (62) - 神奇的序列化

    本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...

  4. Java编程的逻辑 (64) - 常见文件类型处理: 属性文件/CSV/EXCEL/HTML/压缩文件

    ​本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...

  5. Java编程的逻辑 (60) - 随机读写文件及其应用 - 实现一个简单的KV数据库

    本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...

  6. Java编程的逻辑 (51) - 剖析EnumSet

    本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...

  7. Java编程的逻辑 (90) - 正则表达式 (下 - 剖析常见表达式)

    本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...

  8. Java编程的逻辑 (87) - 类加载机制

    本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...

  9. Java编程的逻辑 (56) - 文件概述

    本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...

随机推荐

  1. 【Webpack2.X笔记】 配合react项目进行配置

    前言: 本文是自己在工作中使用webpack进行react开发项目构建的一些经验总结,做以记录防范后续踩坑. 如果您还没有webpack相关基础,请先移步 入门Webpack,看这篇就够了 进行基础学 ...

  2. js保留两位小数方法总结

    js保留两位小数方法总结 最近在做结算系统,经常需要用到金额保留两位小数,刚开始我一直用的是Angular中的过滤器number |2,但是,这无法满足我的需求.问题是,当用户离开文本框时,我需要将用 ...

  3. 【模板】Floyd

    int n; ][MAX_N + ]; void Floyd() { ; k <= n; ++k) { ; i <= n; ++i) { ; j <= n; ++j) { d[i][ ...

  4. HGOI20180831 NOIP2018模拟

    input1: 4 4 4 4 4 3 2 4 5 4 5 5 5 1 7 3 2 output1: Yes Yes Yes No 好的吧数学题QwQ考场上没人做出来qwq 就是判断两个矩形能否互相放 ...

  5. POJ 3436 ACM Computer Factory (网络流,最大流)

    POJ 3436 ACM Computer Factory (网络流,最大流) Description As you know, all the computers used for ACM cont ...

  6. virtualbox 迁移虚拟机存储位置

    1. 菜单--管理--全局设定 ,更改 默认虚拟电脑位置. 2. 复制 (移动)现有虚拟机目录到新位置,软件里删除现有虚拟机 3. 菜单--控制--注册,逐个选择虚拟机目录里的 .vbox文件,导进虚 ...

  7. MySQL的DML常用语法格式

    MySQL的DML常用语法格式 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 我们知道MySQL的查询大致分为单表查询,多表查询以及联合查询.多表查询,顾名思义,就是查询的结果可能 ...

  8. TradingView学习记录

    官网:https://cn.tradingview.com   申请图表库 用本地服务器打开 二:文件目录 三:基础概念 3.1 UDF:通用数据饲料(Universal Data Feed)     ...

  9. MySql与对应的Java的时间类型

    MySql的时间类型有          Java中与之对应的时间类型date                                           java.sql.Date Date ...

  10. Codeforces 932 E. Team Work(组合数学)

    http://codeforces.com/contest/932/problem/E 题意:   可以看做 有n种小球,每种小球有无限个,先从中选出x种,再在这x种小球中任选k个小球的方案数 选出的 ...