学校课程并没有专门教Java,所以自己整理了一些基本Java语法之类的东西,还涉及一部分操作系统、计网、数据库的大山中学时做的笔记。另外结合Geeks4geeks做一些补充。
Java基础
Java简介
- Java版本分为三种
-> Java SE: 标准版,适用于桌面系统;
-> Java ME:微缩版,适用于小型设备;markdown tablemarkdown table
-> Java EE:企业版,适用于创建服务器应用程序和服务。 - JDK和JRE的区别:
-> JDK:开发套件,提供了Java的开发环境,是面向开发人员使用的SDK;
-> JRE:执行环境,提供了Java的运行环境,是面向程序的使用者。 - SDK和API的区别:
-> SDK:software development kit, a collection of implementation tools.
-> API:application programming interface, defines how to call another service.
Java编译、执行:
- Java的文件名必须与代码中的public类名相一致。若类不是public则不必要,但注意一个文件中的多个类只能有一个用public修饰,编译后每一个类对应一个.class文件。
- main是程序的入口方法,必须在Java程序中定义,它的签名是固定的。main可以定义在任何一个类中,不一定是public修饰的那个类。
- 编译命令:
javac –d . .HelloWorld.java
。其中-d
表示生成类的时候按照包结构生成文件夹,.
表示在当前路径下开始生成。 - 执行命令:java myPackage.HelloWorld。
包名.类名
的格式,将可执行字节码文件交给JVM执行。 - 运行过程:源码文件
.java
经过编译器javac后生成与平台无关的字节码文件.class
,最后交给Java解释器执行字节码。总结起来就是『先编译,后解释』。编译指的是生成可执行文件(效率高、不跨平台),解释指的是将源文件逐行解释(效率低、跨平台)。 - Java是跨平台的语言,Write Once and Run Anywhere(WORA)。真正执行的不是二进制代码,而是字节码,由JVM负责解释执行。Java跨平台的特性其实是基于JVM,但JVM本身是C语言编写的、并不是跨平台的。所以Java能够跨平台,本质上是依靠了不跨平台的JVM。
Java面向对象
- 面向对象是一种程序设计方法、一种程序设计范型,基本思想是使用对象、类、继承、封装、消息等概念来设计程序。面向对象强调以问题域中的事物为中心来思考问题、认识问题,根据这些事物的本质特点,把它们抽象地表示为系统中的对象作为系统的基本构成单位。如此一来,系统便可以直接映射问题域,保持问题域中事物及其相互关系的本来面貌。
- Object: represents an entity with unique identity, state(attributes), behavior(operations).
- state: condition where the object exists.
- behavior: how the object acts to requests from other objects.
- Class: description of a set of objects with same stucture(attributes) and behavior(operatios), serving as template for creating objects.
- 面向对象的四个主要特征:抽象abstraction、封装ecapsulation、分层hierarchy、模块化Modularity。
- 抽象:Essentail characteristics of an entity reltive to a given perspective, eliminating details. 只关注事物的相似点、共性,忽略与当前主题无关的方面。例如每个人都有不同,但我只关注姓名、年龄、收入这些内容,那就可以抽象成一个人,而不必管其他身高体重之类的无关数据。通过抽象可以简化、提炼问题的本质。
- 封装:grouping of related elements into single entity, helping to hide information. 希望程序的部件”高内聚、低耦合”,防止程序相互依赖而带来的变动影响。在面向对象编程中,对象是封装的基本单位,面向对象的封装就是把描述一个对象的属性和行为的代码封装在一个类中。其中属性用变量定义,行为用方法定义,方法可以直接访问同一对象中的属性。通常将属性定义为private,这样就可以思考具体的方法究竟要定义到什么类中。例如人在黑板上画圆这个过程,显然圆心和半径都是圆的私有属性,所以画圆必须定义到圆这个类中,人只是负责发送一个消息给圆让它画一个。
- 分层:分为组合aggregation(is-part-of)和继承inheritance(is-a-kind-of)。继承是子类自动共享父类数据和方法的机制,是类之间的一种关系,提高的代码的可重用性和可扩展性。在定义和实现一个类的时候,可以利用已有的类,将它的内容作为自己的基础内容,然后再添加一些新的内容或修改覆盖原有内容。通过继承,我们实现了多态polymorphism,多态指的是程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编译时并不确定,在程序运行时才会确定。程序真正运行时才能确定引用变量到底会指向哪个类的实例对象、调用谁的方法。如此一来,就可以改变程序运行时绑定的具体代码,让程序可以选择多个运行状态。
- 模块化:decomposition of a large entity into smaller modules, which are independent, cohesive. Low coupling, high cohesion.
Java的数据类型
- 基本数据类型:8种
-> 整形:byte(1), short(2), int(4), long(8)
-> 浮点型:float(4), double(8)
-> 字符:char(2)
-> 布尔:boolean
-> 字符类型要用单引号括起来,\u0000表示将字符转换为对应unicode编码。
-> 数字默认浮点型为double,强制用0.12f。
-> Long类型也需要加123l(l = L). - 引用数据类型:类、接口、数组12// 引用类型 变量名 = new 类名(参数)String str = new String("hehe");
Java内存空间分配与回收
- 栈:存放简单数据类型变量(值+变量名),存放引用数据类型的变量名+所指向实例的首地址。栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。
- 堆:存放引用数据类型的实例本身。JVM只有一个堆区(heap)被所有线程共享。
- 方法区:静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量。
- 垃圾回收:用后台线程gc进行垃圾回收。只有JVM发现内存不够的时候才会中断代码运行并进行垃圾回收。垃圾指的是不再被引用的对象如局部变量、没有指针指向的对象。
局部变量、实例变量、静态变量
- 局部:不是出现在类体花括号中的变量。必须先赋值才能使用。
- 实例:属于某个对象的变量,必须实例化对象之后才会分配空间(在堆,和对象在一起)、才能使用。
- 静态:直接属于一个类,又称类变量,只要程序加载类字节码就会给静态变量分配空间(在方法区),直接通过类名访问、使用。类的静态变量在内存中只存在一个,被所有类的实例共享,只能通过类名访问而不能通过特定的对象访问。Main方法必须声明为static的,这样就可以不通过对象而直接在类加载的时候找到程序入口,执行程序。
包
- Package:通常以公司域名的反转为报名,每个字母小写。
- 目录结构:在环境变量设置中会有一个默认包的位置,要想使用自己定义的包及其下属的类,就需要指定包的目录结构,保证.class文件在对应目录中,执行时需要加上
java com.bobby.HelloWorld
这样。在其他程序中也可以通过包名导入类,如import com.bobby.HelloWorld;
数组
数组是对象,是有序数据的集合。声明数组并没有真的创建一个对象,而是要通过new来创建对象。声明时不需要指定长度,new时才需要指明空间。
12int[] I = new int[2];Student[] s = new Student[10];数组创建后元素有初始值,对象则会调用无参数的构造子。
- 二维数组其实是一维数组中每个元素又分别是一个一维数组。高维要先分配好空间才能给低维数组分配(否则就是空中楼阁)。
- 数组拷贝:使用系统类System提供的
Static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);
srcPos表示源数组的什么位置开始拷贝,destPos表示放到dest的那个位置开始。 - 数组排序:可以用
java.util.Arrays.sort(object obj, Comparator myComparator);
自定义排序可以通过自定义MyComparator来实现。或者直接声明匿名类,甚至使用lambda。123456789101112131415161718192021class MyComparator implements Comparator {public int compare(Object obj1, Object obj2) {User u1 = (User)obj1;User u2 = (User)obj2;if (u1.age < u2.age) {return -1; // 升序排列} else if (u1.age > u2.age) {return 1; // 降序排列} else { // 年龄相同按String排序return u1.name.compareTo(u2.name);}}}// actually we don't need to explicitly define thisArrays.sort(arr, new Comparator() {public int compare(Node a, Node b) {return a.val - b.val;}});// and more easilyArrays.sort(arr, (a, b) -> a.val - b.val);
然后在public的类的main中ArrayList arrayList = new ArrayList();
之后,在往里add User的实例对象。Object[] users = arrayList.toArray();
之后就可以java.util.Arrays.sort(users, new MyComparator());
这样调用了。
Java的串行化(序列化)
- 串行化可以使你将一个对象的状态写入一个Byte流中,并且可以从其他地方把该Byte流的数据读出来,重新构造一个相同的对象。这样就可以将对象通过网络进行传播、随时把对象持久化到数据库、文件系统中。
- 序列化就是一种处理对象流的机制,将对象的内容进行流化,可以对流化后的对象进行读写操作,也可通过网络传输。
final
- 类似于const,表示不许改变。修饰变量、方法、类。
- final修饰的变量称为常量,没有默认初始值,必须进行一次直接赋值或者在构造方法里赋值,只能赋值一次。而在前面再加上static,则只能直接赋值。而final修饰的引用变量指的是引用不能随意改变指向,但是指向的那个对象本身还是可以变的。
- final修饰的方法保持方法稳定性,不允许被子类覆盖。
- final修饰的类不能被继承,类中的方法都是final的。
- final, finally, finalize区别
-> final用于声明属性、方法和类,分别表示属性不可变、方法不可覆盖、类不可继承。内部类要访问一个局部变量,则该局部变量必须定义为final类型。
-> finally是异常处理末尾的统一出口,总是执行。
-> finalize是Object类的一个方法,在垃圾回收执行时调用被回收对象的finalize方法,可以覆盖重写以进行其他资源的回收如关闭文件等。
abstract
- 抽象类:含有抽象方法的类都是抽象类(需要声明abstract关键字),但是抽象类其实也可以全是具体方法。抽象类不能生成对象实例,但可以作为对象变量声明的类型,只能定义一个引用,指向属于该抽象类的子类的实例。抽象类相当于半成品,需要子类覆盖其中的抽象方法。
- 抽象方法:定义了一种标准,不含有具体实现,子类负责继承并实现父类所有抽象方法后才能实例化。
- abstract和final不能同时使用。abstract修饰明摆着就是需要继承、重写的,这与final相违背。private也不能同时使用,因为private直接就阻止了继承和重写。
- Java不支持多继承,因为可能出现在父类中定义了相同的方法(method with same signature),然后同时继承,调用该方法的时候就不知道调用的是哪一个方法了(diamond of death)。或者解释说这是一个is-a问题,不能同时是同等级的东西。
接口interface
- 接口本质上是特殊的抽象类。通过接口可以间接实现多继承,而不会增加类关系的复杂度。因为接口是对类的进一步抽象,可以分出主、次关系类型。接口定义了一种标准,接口的调用者是在使用这种标准,接口的实现类则是在实现标准。通过接口可以将使用标准和实现标准分离开来,解除耦合关系,具有可移植性。
- 在接口中,所有的方法都是public abstract、所有的属性都是public static final。
- 接口与接口之间可以多继承,extends后用逗号隔开。这是因为接口仅仅定义了一种约定contract,仅仅是方法的signature而不涉及implementation,这样在子接口中发现相同的signature,直接归为一类。接口没有构造子,也没有main方法,不过在Java8中出现了接口的default方法,也允许一部分带有implementation的方法被继承,在子类implement这些接口的时候,需要手动调用
I1.super.func1()
来防止编译器不知道是哪个接口的方法。 - 接口的实现是通过类。
public class className implements itf1, itf2, itf3
来实现接口,允许实现多个接口。类必须实现接口中所有的方法,否则仍为抽象类。实现类中的方法必须加上public。
包装类及其拆箱装箱
- 前面提到了八种基本数据类型,都是值类型,并不是类。它们都有对应的引用数据类型,即类类型。
装箱:把基本类型用它们对应的引用类型包起来,具有对象的特征。自动装箱则是每当需要一种类型的对象时,便可自动把基本类型封装到对应的包装中。
123int i = 11;Integer i =I; // 自动装箱int k = I; // 自动拆箱拆箱:把引用类型的对象重新简化成值类型的数据。自动拆箱则是每当需要一个值时,被装箱的对象中的值就会提取出来而无需显示调用intValue()方法。
IntegerCache类缓存:为了提高效率,IntegerCache类中有一个static数组缓存了-128~127的Integer对象,在自动装箱时若真实值落在这个范围内,则直接从Integer数组中取出来使用,这样直接比较对象的引用,也会发现是相等的。但是手动new出来的就不会有这个cache的事了。当超出这个范围后,即便自动装箱也不会有cache的事了。
123456Integer a = 100;Integer b = 100;a == b // trueInteger c = 200;Integer d = 200;c == d // false更有意思的是,原本是new出来的对象,经过自动拆箱再自动装箱,又会用到cache了:
12345Integer m = new Integer(5);Integer n = new Integer(5);m == n; // falsem = m – 1; n = n – 1;m == n; // true==
,!=
比较的是引用对象的地址而不会自动拆箱;>
,>=
,<=
,<
,则会触发自动拆箱取出对象中的值。要比较Object的值,还是需要调用equals或实现compareTo方法为妙。
String, StringBuffer, StringBuilder
String:不可改变的Unicode字符序列。在String类中,以”xxx”的字面值创建时,直接到Java方法空间的串池中查找,若已存在则直接返回该串地址,若无则新建。
123String s1 = "abc"; // 新建String s2 = "abc"; // 直接从池中取出String s3 = new String("abc"); // 在堆中创建,不管池调用String类的任何方法都不会改变String对象本身。
- StringBuffer:可改变的Unicode字符序列。synchronous允许并发操作,thread-safe是线程安全的。最典型的使用场景是String的连接,使用append方法会比新建String高效。
- StringBuilder:可改变的Unicode字符序列。但不支持并发操作,不是线程安全的,不过就更快。
内部类
- 内部类作为类的一个成员,依附于外部类存在。内部类提供了某种进入其外围类的窗口,通常使用场景是内部类继承自某个类或实现某个接口,在内部类代码中操作创建其外围类的对象。无论外围类是否已经继承了某个接口,都对内部类没有影响。主要分为成员内部类、局部内部类、静态内部类、匿名内部类。
- 成员内部类:作为一个成员,与外部类的属性、方法并列。在内部类中可以访问外部类的所有成员,不能定义静态成员。依附于外部类的具体对象,才能生成内部类对象。
- 局部内部类:在方法中定义的内部类,不再是外部类的一个成员,但仍需依附于外部类的对象。
- 静态内部类(嵌套类):允许存在独立于外围类对象存在的内部类对象。而在普通内部类中隐含地保存了一个引用,指向创建它的外部类对象。静态内部类可用public, protected, private修饰,内部可定义静态或非静态成员,但只能访问外部类的静态变量&静态方法。
- 匿名内部类:当只需要用到类的一个实例、定义后马上就用到、类本身非常小、给类命名并不会增加代码可读性,就推荐使用匿名内部类。匿名内部类本身是局部内部类,但它不含构造子、不能定义任何静态成员、方法或类、它不能是static, public, private, protected修饰的、一定跟在new后面。
异常
- try + catch + finally
assert
- 要想使用断言,在执行时需要java –ea 类名这样强制开启。
- 断言保证了最基本的、关键的正确性。
assert 布尔表达式 字符串;
一旦这个表达式为false,后面的字符串就会显示出来。
equals与==
在Object类中,equals的定义就是==
,表示的是判断两个变量指向的对象是否是同一个变量。所以如果希望比较的是对象的内容,还是需要自己覆盖equals方法。
overload重载与override覆盖重写
- 重载表示同一个类中可以有多个相同名字的方法,只是参数列表不同(类型or个数)。
- 覆盖(重写)表示子类的方法可以与父类的方法的名字和参数列表完全一样,但是在子类中重新定义,这样通过子类创建对象时就会调用子类的这个方法,这是多态的一种表现。
Abstract与interface
- 抽象类不能创建实例对象,其中的方法不必是抽象的。其抽象方法必须在具体子类concrete中实现。抽象类不能有抽象的构造子或静态方法。
- Interface可看作抽象类的特例,接口中的所有方法都一定是抽象的。默认方法均为public abstract修饰,默认成员变量都是public static final.
interface | abstract class |
---|---|
方法必须是抽象的 | 可含抽象/非抽象方法 |
成员变量必须final,可含有static | 可含final/static/普通成员变量 |
接口只能extend另一个接口 | 抽象类可extends另一个类 或implements多个接口 |
深拷贝与浅拷贝
- 浅拷贝(Object类中的clone()方法)是指在拷贝对象时,对于基本数据类型的变量会重新复制一份,而对于引用类型的变量只是对引用进行拷贝。它只是使用一个已知实例对新创建实例的成员变量逐个赋值,只复制基本变量值、传递类对象引用,不能完全复制实例。
- 深拷贝则是对对象及该对象关联的对象内容,都会进行一份拷贝。基本变量的值是肯定会复制的,而引用则不仅仅是复制引用,而引用指向的对象也会创建一个相同的对象。
- Clone方法:定义在Object类中,将对象复制了一份并返回给调用者。在派生类中覆盖基类的clone()方法,并声明为public。在派生类的clone()方法中,调用super.clone()。在派生类中实现Cloneable接口。
参数传递
Java中的方法都是都是按值传递的,所以对于基本数据类型是无法修改从外部传入的变量的(联想C/C++中接触过的swap)。但是对于引用类型,引用变量本身的指向就算在方法里面指向一个new的对象,最终这个引用变量的值确实不会变更的,但是通过引用指向的对象本身还是可以更改的,你在方法中调用一些set函数之类的,都是有效的。
swap
由于java中的参数都是按值传递的,所以肯定没法像C/C++一样用一个&搞定。但是Java中的Integer类又不允许直接修改里面的属性Int。有几种间接方法:
自定义MyInteger类
1234567891011121314151617class MyInteger {private int value;public MyInteger(int i) {this.value = i;}public void setValue(int value) {this.value = value;}public int getValue() {return this.value;}public static void swapMyInteger(MyInteger a, MyInteger b) {Myinteger c = new MyInteger(a.getValue());a.setValue(b.getValue());b.setValue(c.getValue());}}数组的形式
12345public static void swap(int[] data, int a, int b) {int t = data[a];data[a] = data[b];data[b] = t;}外部内联
12345678910111213141516171819202122public class Swap2 {public static void main(String args[]) {Swap2 sw = new Swap2(1,2);System.out.println("i is" + sw.i);System.out.println("j is" + sw.j);sw.swap();System.out.println("i is" + sw.i);System.out.println("j is" + sw.j);}int i, j;public Swap2(int i, int j) {this.i = i;this.j = j;}public void swap(){int temp;temp = i;i = j;j = temp;}}Collections.swap
class.forName(String ClassName)的作用
- 返回与给定的字符串名称相关联类或接口的Class对象。它是static的方法,不依赖Class类对象。Class类没有显式的构造子可以调用,只有类加载器的defineClass自动调用。
- Java程序在运行时,Java运行时系统一直对所有的对象进行所谓的运行时类型标识。这项信息纪录了每个对象所属的类。虚拟机通常使用运行时类型信息选准正确方法去执行,用来保存这些类型信息的类是Class类。Class类封装一个对象和接口运行时的状态,当装载类时,Class类型的对象自动创建。
ClassLoader如何加载Class
- ClassLoader用于将Class文件加载到JVM,使java动态加载类定义的特性成为可能。
- 当运行一个程序的时候,JVM启动,运行bootstrap classloader,加载java核心API,再调用ExtClassLoader加载扩展API,最后AppClassLoader加载CLASSPATH目录下定义的class。
- 这个bootstrap classloader不是用java编写的。它在JVM运行时加载java核心API以满足java程序最基本的需求。
- 类加载的双亲委托模式:每一个自定义的ClassLoader都继承ClassLoader这个抽象类,同时还有个parent指向父类。这个Parent不是真正的被继承的父类,而是在实例化ClassLoader时指定的一个ClassLoader。若这个parent是Null,则默认其父类为bootstrap ClassLoader。当parent无法加载成功才会由自己的findClass加载。所以我们要实现一个自定义类时,只需要实现一个findClass方法即可。
- 双亲委托模式作用:一是避免重复加载,当parent已经可以加载该类,就没必要让子ClassLoader再加载一次;二是考虑安全因素,保证核心API中定义的类不会被轻易替换掉,例如String一定会现在Bootstrap ClassLoader中加载好,轮不到自定义的findClass.
- JVM加载类:装载、连接、初始化。装载是找到相应的class文件并读入JVM,连接分为三步:验证class是否符合规格、为类变量分配内存并设置默认初始值、解释(可选),即根据类中的符号引用查找相应的实体,再把符号引用替换成一个直接引用的过程。
- 使用forName加载一定会将Class进行解释和初始化;loadClass加载类时默认不进行解释和初始化
。hashCode方法的作用
- 根据对象的内存地址换算出的一个值。利用哈希可以提高从集合中查找元素的效率,它将集合分为若干个存储区域,每个对象都尅算出一个哈希值,再将哈希值分组,每组对应某个存储区域。
- 结合equals可以帮助判断在集合(list或set)中是否存在某元素。在调用hashCode之后若该物理地址上没有元素则直接存入,若有则还需要用equals判断元素是否相同。注意,hashCode相同不一定就能确定两元素相同。
- Equals和hashCode都是从Object类中继承来的,但通常都会在子类中覆盖掉。Equals改写时应满足对称性(x = y, y = x)、反射性(x = x)、类推性(x = y, y = z)、一致性(持久x = y)、对象不合全假性(x != null, x != 不同类对象)。
- hashset:继承Set接口,Set接口又实现的是Collection接口。Hashset不允许重复对象,对象顺序也不确定。在hashset中判定两个元素是否重复就是通过(1)hashCode是否相等(相等则进行(2));(2)使用equals运算判断。
JVM
JVM的学习有两个重要部分:Java代码编译和执行的整个过程 & JVM内存管理和垃圾回收机制。
- Java代码的编译由Java源码编译器完成:
生成的JVM字节码的执行由JVM执行引擎完成: - Java代码编译和执行的整个过程包含三个重要机制:
-> Java源码编译机制:三步走——分析和输入到符号表、注解处理、语义分析和生成class文件。最后生成的class文件包含了结构信息(class文件格式、版本号、各部分的数量和大小信息)、元数据(对应于java源码中声明和常量信息,包括类、父类、实现的接口的声明信息、域和方法声明信息和常量池)、方法信息(对应java源码中语句和表达式对应的信息,包括字节码、异常处理器表、求值栈和局部变量区大小、求值栈的类型记录、调试符号等信息)。
-> 类加载机制:通过ClassLoader及其子类完成,加载顺序依次为
-Bootstrap ClassLoader(加载$JAVA_HOME中jre/lib/rt.jar的类,由C++实现而非ClassLoader子类)
-Extension ClassLoader(加载$JAVA_HOME中jre/lib/*.jar的其他扩展功能包)
-App ClassLoader (加载classpath指定的jar包)
-Custom ClassLoader(应用程序根据自身需要自定义的ClassLoader)
最终会从下往上检查是否所有类都加载成功。
-> 类执行机制:基于栈来执行class字节码。线程创建后,会产生程序计数器pc和栈stack,pc存放下一条要执行的指令在方法中的偏移量,栈中存放一个个栈帧,每个栈帧对应每个方法的每次调用,栈帧由局部变量区(存方法中的局部变量和参数)和操作数栈(存放方法执行时的中间结果)两部分组成。 - JVM内存管理
JVM栈由堆、栈、本地方法栈、方法区等部分组成。
-> 堆:存放对象内容,所有new出来的内容都在堆中分配。堆分为新生代和旧生代,其中新生代存放新建对象于eden区,当edne空间不足是会移到survivor取。旧生代存放多次垃圾回收后仍然存活的对象。
-> 栈:每个线程执行每个方法的时候都会在栈中申请一个栈帧。
-> 本地方法栈:用于支持native方法的执行,存储了每个native方法调用的状态。
-> 方法区permanent space:存放了要加载的类信息、静态变量、final类型的常量、属性、方法信息。
JVM垃圾回收
JVM对新生代和旧生代采用不同的垃圾回收机制。
- 生代通常存活时间较短,故用Copying算法扫描出存活的对象并复制到一块新的完全未使用的空间(Eden的From Space复制到To Space)。新生代GC的触发由空闲指针的方式控制,指针记录最后一次分配的对象在新生代区间的位置,当下一次又要分配内存时就会检查空间是否足够,不够就触发GC。当连续分配对象的时候,对象会逐渐从eden到survivor,最后到旧生代。
- 旧生代对象存活时间较长,故采用Mark算法扫描出存活的对象并回收未被标记的对象,回收后对空间合并或标记为空闲方便下次分配。
- Main objective: free heap memory by destroying unreachable objects.
- Mark and sweep algorithm: when objects are created, their marked bit is set to false. when gc starts, mark phase will DFS from root(assume one only) to all the other nodes and mark the reachable ones as true; in sweep phase, all the nodes that are marked false will be swept away from heap and those marked true will be reset as false.
多线程
进程与线程
- 进程:同一个OS中执行的一个程序,包含了虚拟CPU、代码、数据。进程是资源的拥有者,是资源分配的基本单元。
- 线程:在同一进程当中执行的子程序流。线程是进程中执行的程序片段,是调度的基本单元。
- 进程与线程:有点类似包含关系,线程从属于进程的一部分。进程之间是独立的,这表现在内存、上下文环境上,不能去占用其他进程持有的空间;而同一进程下的线程则共享一片内存空间。具体地,进程拥有独立的进程空间,数据存放空间栈空间、堆空间都是独立的;而线程的堆空间是共享的,栈空间是独立的,线程消耗的资源少于进程,相互之间会有影响。
- 多进程:同一个OS中执行的多个并行的程序,可以提高CPU利用率。
- 多线程:同一个进程中并发执行的多个子程序流,可以提高CPU利用率。
- 只有运行状态的线程才有机会执行代码,主线程的中止(main方法退出)不会影响其他正在运行中的线程,在线程内部,程序按顺序执行。只有进程中的所有线程都中止,进程(JVM进程)才会退出。
线程编程
写一个类extends Thread类,并覆盖其中的run()方法。之后在main中实例化对象后再.start()即可运行。由于Java不支持多继承,所以在这种情况下就没法再继承别的基类了。不过可以访问Thread类中定义的yield, interrupt等方法。
1234567class MyThread extends Thread {public void run() {...}}Thread t1 = new MyThread();T1.start();写一个类implements Runnable接口,实现其中的run()方法。但是这个类不能直接作为对象开启线程,而是作为线程Thread的对象构造子的参数实例化一个线程后,还是通过线程来开启。由于这只是实现了接口,因此还可以继承某种别的基类或实现若干别的接口。
12345678class MyThread implements Runnable {public void run() {...}}Runnable myThread = new MyThread();Thread t2 = new Thread(myThread);T2.start();
线程的生命周期
- 初始状态new:创建了一个线程对象,线程只是处在JVM中,并没有真正开始运行。
- 就绪状态ready/runnable:已经具备了运行条件,但还没有分配到CPU。处于就绪队列中(或者叫可运行池)等待CPU调度。
- 运行状态running:拥有CPU的执行权。
- 阻塞状态waiting:运行状态中的线程若需要等待I/O资源或者调用了sleep等方法,就会进入阻塞状态。阻塞之后的线程会重新进入就绪队列等待调度。
- 终止状态terminate:线程运行结束。所有线程都over了这个进程才会over。
- 等待队列状态:一个线程调用一个对象的wait(),就会放弃该对象的锁标记,进入等待队列状态。当其他线程调用临界资源的notify或notifyAll就会将等待队列的线程释放进入锁池状态。
- 锁池状态:每个对象都有互斥锁标记,防止对临界资源的访问造成数据的不一致、不完整性。一个线程拥有一个对象的锁标记后,其他线程若想访问这个对象,就需要在锁池中等待,由OS决定将锁标记分配给哪个线程。线程拿到锁标记后进入就绪状态等待调度。
多线程模型
- User thread:受内核支持,受用户级线程库管理;
- Kernel thread:直接受内核支持和管理。
- 分三类模型:多对一、一对一、多对多。
-> 多对一:多个user映射到一个kernel; 对用户级线程的控制可通过用户级线程库实现,不需要模式切换;
-> 一对一:user与kernel thread一一对应;可并行处理,即多核同时处理一个进程,且一个线程的挂起不会导致多个线程的阻塞。
-> 多对多:将多个user thread映射到小于等于其个数的kernel thread; 可并行处理,即多核同时处理一个进程,且一个线程的挂起不会导致多个线程的阻塞。
多线程同步
- 多线程并发访问同一个对象,若不对线程进行同步控制,则会破坏原子操作导致临界资源的数据不一致。竞争条件:多线程并发访问、操作同一数据块且执行结果与访问的顺序有关。解决临界区冲突应满足:互斥、前进、有限等待。
- 每一个对象都有一个互斥的锁标记和一个锁池。只有线程拥有这个锁标记才能访问这个资源,其他线程都在锁池中等待。这种等待状态若陷入无法改变的情况,就形成了死锁。死锁的四个条件:互斥、占有且等待、非抢占、循环等待。
- Synchronized:在方法范围内对当前对象加锁,只有拿到对象锁标记的线程才能执行这个方法。锁对象相同,同步语句串行;锁对象不同,同步语句并行。
- 多线程编程的优势
-> Responsiveness:程序可以在部分线程阻塞的情况下运行。
-> SourceSharing:同一进程下的线程共享资源,可节约空间并方便访问。
-> economy:创建线程并不涉及大量资源分配,开销小。
-> 多核的使用:线程可以并发运行,使多核处理器能物尽其用。 - Wait()方法:调用加锁的对象使用wait会使当前持有对象锁标记的线程(wait在syncrhronized方法中因此该线程必定持有锁)释放锁标记并进入该对象的等待队列(这还不是锁池!)。若此时另一个线程获得了该对象的锁标记,并进入代码块调用notify(),则会放出等待队列中的一个线程进入锁池。
- Notify()方法:从对象的等待队列中移出任意一个线程(由系统决定)放入锁池,等待锁标记。
- NotifyAll()方法:移出所有等待队列中的线程放入锁池。
Sleep与wait方法
- sleep:Thread类中的方法,表示线程休眠,会自动唤醒。Sleep只是将执行机会让给其他线程而不会释放对象锁,监控状态依然保持、之后会自动恢复。
- wait:Object类中的方法,表示当前线程释放对象锁并进入等待队列,待针对此对象的notify方法执行后才会进入锁池。
当一个线程进入一个对象的一个synchronized方法后,其他线程可否进入该对象的其他方法?
分情况来看。
- 若其他方法无synchronized关键字修饰,则可以;
- 若该sychronized方法内部有调用wait,则可以进入其他synchronized方法;
- 若其他方法都有synchronized关键字修饰且当前synchronized方法没调用wait,则不能;
- 若其他方法是static,则它用的同步锁是当前类的字节码,不能与非静态的方法同步,因为非静态方法用到的是this必须有对象。
Synchronized与java.util.concurrent.locks.Lock异同?
- 同:Lock可以完成synchornized的所有功能;
- 异:Lock有比synchronized更精确的线程语义和更好的性能。Lock要求手动释放锁、必须在finally中释放,而synchronized是自动释放锁。
进程间通信
- 管道(pipe)及有名管道(named pipe):管道可用于具有继承关系的父子进程间的通信,有名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。
- 信号(signal):信号是在软件层次上对中断机制的一种模拟,它是比较复杂的通信方式,用于通知进程有某事件发生,一个进程收到一个信号与处理器收到一个中断请求效果上可以说是一致的。
- 消息队列(message queue):消息队列是消息的链接表,它克服了上两种通信方式中信号量有限的缺点,具有写权限得进程可以按照一定的规则向消息队列中添加新信息;对消息队列有读权限的进程则可以从消息队列中读取信息。
- 共享内存(shared memory):可以说这是最有用的进程间通信方式。它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。
- 信号量(semaphore):主要作为进程之间及同一种进程的不同线程之间得同步和互斥手段。
- 套接字(socket):这是一种更为一般的进程间通信机制,它可用于网络中不同机器之间的进程间通信,应用非常广泛。
线程间同步通信
- 信号量
- 消息
- 事件
集合框架Collection
综述
- 结构
- Collection是最基本的集合接口,一个Collection代表一组Object,称为Collection的元素。保存的必须是对象而不能是简单类型。
- ArrayList实质上是长度会增长的数组。查询效率高、增删效率低。加载大批量数据时,可以用
ensureCapacity(int minCapacity)
手动扩容以提高效率。 - HashSet:Object类中的hashCode方法是所有类都会继承的方法,用于计算一个Hash码值返回,HashSet就用这个哈希值基于数组的长度取模,这个模值就是存放的下标,若这个下标已经有对象则需要判断这两个对象是否相同,不同则需要另找位置添加。故对于自定义类来说调用HashSet就需要同时覆盖hashCode()和equals()方法。
- TreeSet:可排序的Set。SortedSet接口是Set的子接口,TreeSet是SortedSet接口的实现类,他可以对集合中的元素进行排序。为了实现自定义类的排序,需要实现java.lang.Comparable接口(可省去hashCode方法)。接口只定义了compareTo(Object o)这个方法,返回值是整型,小于参数对象返回-1,大于返回+1. 另外需要注意的是java.util.Comparator接口,
compare(Object o1, Object o2)
其实跟前面也差不多,创建集合时把定义好的比较器也作为参数便可构造有序集合。 - HashMap:基于哈希表的Map接口的实现,提供键值映射,允许空值和空键null,非线程安全,不支持并发控制. HashMap的遍历首先是调用keySet获得所有键,遍历这个集合根据key拿到value。
- Hashtable:不允许空值和空键null,线程安全,支持并发控制。一般不用。
- SortedMap接口:Map的子接口,可根据键排序规则整理加入的键值对,排序规则类比SortedSet。
- Key通常是八种基本数据类型的对应类以及String,自己定义的类作为Key没有意义。
ArrayList与Vector
- 同:都实现了List接口,都是有序(有索引)集合,随机访问效率高,数据允许重复。
- 异:
-> 同步性。Vector是旧版本,保证了线程安全,它的方法之间是线程同步的;ArrayList是线程不安全的。若只有一个线程,显然用ArrayList更高效,因为它根本不考虑线程同步问题。(同步性问题Vector&ArralList – Hashtable&HashMap可以类比,都是一老一新,老的能保证线程安全)
-> 数据增长。当元素超出容量时,Vector一次增长一倍,ArrayList增加0.5倍。他们都可以设置初始长度,但这个增加长度只有Vector可以设置。
List, Map, Set三个接口存取元素时各有什么特点?
- Set:单列元素的集合,父接口为Collection。不允许重复元素,重复不仅指相同更指相等equals。故存入的add方法会调用equals方法遍历集合判断是否已经有相等对象。取元素不是按索引取,而是要通过Iterator接口取得所有元素遍历。而HashSet通过hashCode值的某种运算方式进行存储,而不是直接根据hashCode结果作为存储顺序。
- List:单列元素的集合,父接口为Collection。允许重复元素,可以根据索引取元素,随机访问效率高。调用add方法可以指定插入的索引位置,或者说是该位置的索引变量指向了add参数中的对象。除了get(index)根据索引取,List也可以通过Iterator接口取得所有元素遍历。
- Map:双列元素的集合,通过put(Object key, Object value)存入键值对。Key不可以重复。Map.Entry可以获得键值对的集合。
ArrayList, Vector, LinkedList的存储性能和特性
- ArrayList:使用数组的方式存储数据,非线程安全故性能稍优于Vector.
- Vector:使用数组的方式存储数据,使用synchronized方法保证线程安全。
- LinkedList:使用双向链表实现存储,按序号索引数据需要前向或后向遍历,但插入数据只需记录本项的前后项即可,插入效率高。非线程安全。LinkedList提供了一些方法可以当作堆栈和队列使用。
ArrayList的实现原理
- 是List接口的可变数组的非同步实现,提供所有可选列表的操作,允许包括Null在内的所有类作为元素。线程不安全,在多线程访问时需要保证外部同步。
- ArrayList底层本质就是数组。
private transient Object[] elementData;
- 构造子有三种:
->ArrayList()
默认大小为10;
->ArrayList(int initialCapacity)
传入指定初始capacity;
->ArrayList(Collection<? Extends E> c)
构造一个包含指定collection的元素的列表。 存储/添加元素:
->set(int index, E element)
直接替代已有元素;
->add(E e)
加到尾部,add(int index, E element)
加到指定索引处、其后元素均后移一位;
->addAll(Collection<? Extends E> c)
按照Collection的迭代器所返回的元素顺序依次加入尾部;
->addAll(int index, Collection<? Extends E> c)
按照Collection的迭代器所返回的元素顺序依次插入指定索引处。其中需要注意的是index的越界验证(< 0 || > size),添加用到的是系统提供的拷贝方法:System.arraycopy(src, start_src, dst, start_dst, len);
如何知道要将原列表内容拷贝多少到右边?以第四个方法为例:12345678910int numMoved = size – index; //从index往右都有多少元素要挪Object[] a = c.toArray();int numNew = a.length();ensureCapacity(size + numNew);if (numMoved > 0) {System.arraycopy(elmentData, index, elementData, index + numNew, numMoved);System.arraycopy(a, 0, elementData, index, numNew);Size += numNew;return (numNew != 0); // 返回布尔值以示插入成功}读取指定索引元素:直接返回,注意也需要index的越界验证。
删除元素:
-> 给定索引进行删除,右边所有元素都会向左移动一位:123456numMoved = size – index – 1;if (numMoved > 0) {system.arraycopy(elementData, index+1, elementData, index, numMoved);elementData[--size] = null; // 垃圾回收return oldElement;}-> 给定元素进行删除,只会删除首次出现的指定元素:for循环查找索引用equals对比,其实还是按索引删除。注意Null对象是允许存放在列表中的,所以也需要处理null的情况(直接左移)。
- 调整容量ensureCapacity(int minCapacity),只能扩大且至少扩大为1.5倍:关键是末尾调用了一发elementData = Arrays.copyOf(elementData, newCapacity);
也可以将末尾冗余项剔除:trimToSize()方法将length长度缩减为实际size.
HashMap实现原理
- 是基于哈希表的Map接口的非同步实现,提供所有可选的映射操作,允许null值、null键在内的所有类,Key通常是八种基本数据类型的对应类以及String,自己定义的类作为Key没有意义。HashMap不保证映射的顺序,且不保证顺序恒久不变。
- HashMap底层也是数组,它的长度一定是2的幂,而每一项又是一个链表,称之为”链表散列”。事实上,java中最基本的结构就两种,一是数组,二是引用(模拟指针),所有数据结构都能用这两个基本结构来构造。
Transient Entry[] table数组中每一项为Entry,内含键值对和下一个元素的引用:
123456static class Entry<K, V> implements Map.Entry<K, V> {final K key;V value;Entry<K, V> next;Final int hash;}存储元素:put元素时先根据key的hashCode重新计算hash值,然后根据hash值得到这个元素在数组中的索引,若该位置已经存放了其他元素,则新加入的将作为链表头存在此处,最先加入的元素放在链尾。当然这个null要单独处理,例如键为null则放在数组第一位。
1234567891011121314151617public V put(K key, V value) {if (key == null) return putforNullKey(value);int hash = hash(key.hashCode());int I = indexfor(hash, table.length);for (Entry<K, V> e = table[i]; e != null; e = e.next) {Object k;if (e.hash == hash && ((k = e.key) == key) || key.equals(k)) {V oldValue = e.value;e.value = value;e.recordAccess(this);return oldValue;}}modCount++;addEntry(hash, key, value, i); // 这个addEntry就是将新创建的Entry指向原来该位置的元素。return null;}Hash方法:根据key的hashCode重新计算一次散列,加入了高位运算,防止低位不变、高位变化时造成hash冲突。由于HashMap的数据结构是数组和链表的结合,所以当然希望其中的元素位置尽量分布得均匀一些,这样找到这个位置就可以直接判断元素是否符合key而不用遍历整个链表。对于任意对象,只要hashCode返回值相同、丢到hash里计算值也相同。
1234static int hash(int h) {h ^= (h >>> 20)^(h >>> 12);return h ^ (h >>> 7) ^ (h >>> 4);}indexfor方法用于计算对象应该保存在数组的什么索引处。为了让元素分布更平均,首先想到的是将hash值对数组长度取模运算。但是取模的运算消耗过大,实际是采用h & (table.length - 1)来得到该对象的保存位,而HashMap底层数组长度一定是2的幂,故这个运算和取模h % length是等价的。
123static int indexfor(int h, int length) {return h & (length - 1);}当数组长度为2的幂,length – 1所得二进制数各位均为1,这样低位的hash不变,高位的则由hashCode优化处理(???),这使得不同key算得index相同的几率较小。
- 读取元素:首先计算hashCode,再通过hash方法得到索引,找到数组中对应位置的第一个元素,然后往后遍历链表用equals对比key。
- Resize:当HashMap中的元素越来越多时,由于长度不变,hash冲突几率增大,这时就要考虑扩容了。在扩容时,原数组中的所有元素都必须计算其在新数组中的位置,并放到该处。默认扩容的阀值为数组大小的0.75(默认负载因子),达到这么多元素就扩容一倍。
- FailFast:由于HashMap线程不安全,需要用modCount记录修改次数,若当前在使用迭代器时其他线程做了修改,就会抛出异常ConcurrentModificationExcetion. modCount声明为volatile,volatile用于声明简单类型变量,他们的操作为原子级别的。但是当这些简单变量的操作由该变量以前的值相关(n++, n = n*2),则volatile不起作用。
HashSet的实现原理
- 是基于哈希表的Set接口的非同步实现,实际上由一个HashMap实例对象支持。
- 需要重写覆盖equals和hashCode方法,以保证存入对象的唯一性。
LinkedHashMap
- 是Map接口的哈希表和链接列表实现,有可预知的迭代顺序,但不保证映射顺序,不保证顺序持久不变。LinkedHashMap继承了HashMap,底层使用哈希表和双向链表来存储所有元素,原本的数组table也是继承了的。
- Entry与HashMap有所不同,因为它需要同时记录before和next形成双向链表。
I/O
流的概念及基本分类
- 流:直观地理解,读取数据时开启一个通向数据源的流,这个数据源可以是文件、内存、或网络连接;写入数据时则是开启一个通向目的地的流。
- 分类:
-> 根据数据方向分为InputStream/Reader和OutputStream/Writer;
-> 根据数据类型分为字节流InputStream/OutputStream和字符流Reader/Writer. - 字节流:读取时读到一个字节就返回一个字节,可以处理所有数据类型如图片、音频,对文件本身直接操作而不会用到缓冲区的;
- 字符流:使用字节流读到一定字节(1byte-英文, 2byte-中文, 3byte-UTF8)时,先去查指定的编码表,然后将对应的字符返回。字符流只能处理字符数据,通过缓冲区操作文件。
- 使用字节流的优势:硬盘上所有文件都是以字节的形式存储、传输,而字符只会在内存中形成。当然,纯文本数据还是要优先使用字符流的。
常用流用法
- InputStream类为所有字节输入流的父类,有三个基本read方法:
-> int read() 从流里读出一个字节,不推荐使用;
-> int read(byte[] b) 将数据读入一个字节数组中,返回所读的字节数;
-> int read(byte[] b, int off, int len) 定义了offset和len。
此外还有一些方法。int available()返回不受阻塞地从此输入流读取的字节数。 - OutputStream类为所有字节输出流的父类,有三个基本write方法:
-> void write(int n) 将指定字节写入此输出流;
-> void write(byte[] b) 将b.length个字节从字节数组中写入此输出流;
-> void write(byte[] b, int off, int len) …
此外还有一些方法。void flush()刷新此输出流并强制写出所有缓冲的输出字节。 - FileInputStream/ FileOutputStream(详情见后面)
->FileInputStream fis = new FileInputStream("test.txt");
文件必须存在且可读;
->FileOutputStream fos = new FileOutputStream("test.txt");
默认为覆盖模式。想改为追加写入,则在后面跟多一个参数true. - InputStreamReader/OutputStreamWriter
桥梁流,不直接用于输入输出,只是将字节流转成字符流的桥转换器,可指定编解码方式。 - BufferedReader/BufferedWriter
过滤流,需要用其他的节点流来做参数传入构造子。 - PipedInputStream/PipedOutputStream
管道流
File类
- Import java.io.* 导入库
创建文件:
123456File f = new File("D:\\hello.txt");try {f.createNewFile();} catch (Exception e) {e.rintStackTrace();}File类静态常量File.separator和File.pathSeparator。这样就可以让代码跨平台而不必总是修改路径了:
String filename = "D:"+File.separator+"hello.txt";
- 删除文件:
if (f.exist()) f.delete();
- 创建文件夹:
File f = new File("D:\\helloDir"); f.mkdir();
- 列出目录下全部文件:
File f = new File("D:\\"); String[] str = f.list();
- 判断是否为目录:
f.isDirectory();
穷举目录下所有文件,包括子目录:
1234567891011121314public static void print(File f) {if (f != null) {if (f.isDirectory()) {File[] fileArray = f.listFiles();if (fileArray != null) {for (int I = 0; I < fileArray.length; i++) {print(fileArray[i]);}}} else {System.out.println(f);}}}复制文件:
12345678InputStream input = new FileInputStream(file1);OutputStream output = new FileOutputStream(file2);if (input != null && output != null) {int temp = 0;while ((temp = input.read()) != (-1)) {Output.write(temp);}}
字节流InputStream/OutputStream
写入字符串
123456File f = new File(Filename);OutputStream out = FileOutputStream(f);String str = "xxx";Byte[] b = str.getBytes();out.write(b); // (也可以用循环一个一个write(b[i]))out.close();向文件追加新内容
12OutputStream out = FileOutputStream(f, true);Out.write(b); out.close();读取文件内容
123456789File f = new File(fileName);InputStream in = new FileInputStream(f);Byte[] b = new byte[1024];(也可以byte[] b = new byte[(int)f.length]; 避免浪费)in.read(b);in.close();// 也可用in.read的返回值长度确定打印的长度,防止后面输出一大堆空格。// int len = in.read(b);// System.out.println(new String(b, 0, len));使用while读到文件末尾
123while((temp = in,read()) != -1) {b[count++] = (byte)temp;}
字符流Reader/Writer
写入数据
123File f = new File(fileName);Writer out = new FileWriter(f);out.write(str); out.close();追加写入
Writer out = new FileWriter(f, true);
读取字符
12345Char[] ch = new char[100];Reader rdr = new FileReader(f);int count = rdr.read(ch);Rdr.close();System.out.println(new String(ch, 0, count));循环读取
1234int temp = 0;while((temp = rdr.read()) != -1) {Ch[count++] = (char)temp;}
字节/字符转换流OutputStreamWriter和InputStreamReader
OutputStreamWriter将输出的字节流转换成字符流;
InputStreamReader将输入的字节流转换为字符流;
将字节输出流转换为字符输出流
123Writer out = new OutputStreamWriter(new FileOutputStream(file));out.write(str);out.close();将字节输入流转换为字符输入流
12Reader rdr = new InputStreamReader(new FileINputStream(file));Rdr.read(ch);
管道流PipedOutputStream/PipedInputStream
- 用于线程之间的通信
- 例如一个消息发送类、一个消息接受类:12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152class Send implements Runnable {private PipedOutputStream out = null;public Send() {Out = new PipedOutputSream();}public PipedOutputStream getOut() {return this.out;}public void run() {String message = "hey";try {Out.write(message.getBytes());} catch (Exception e) {e.printStackTrace();}}}class Receive implements Runnable {private PipedInputStream input = null;public Receive() {This.input = new PipedInputStream();}public PipedInputStream getInput() {return this.input;}public void run() {Byte[] b = new byte[100];int len = 0;try {Len = this.input.read(b);} catch (Exception e) {e.printStackTrace();} try {Input.close();} catch (Exception e) {e.printStackTrace();}}}class hello {public static void main(String[] args) throws IOException {Send send = new Send ();Receive receive = new Receive();try {Send.getOut().connect(receive.getInput());} catch {e.printStackTrace();}New Thread(sent).start();New Thread(receive).start();}}
打印流PrintStream
|
|
BufferedReader
BufferedReaer只能接受字符流的缓冲区,因为每个中文需要占用两个字节,所以需要将System.in这个字节输入流变为字符输入流:BufferedReader buf = new BufferedReader(new InputStreamReader(System.in));
Scanner类
- 键盘数据输入最常用的方式:Scanner scnr = new Scanner(System.in);
可指定读入的基本数据类型:int t = scnr.nextInt();
另有scnr.nextFloat()之类的. - Scanner可以接受任何输入流。例如从文件中读取:123456File file = new File("xxx.txt");Scanner scnr = null;try {Scnr = new Scanner(file);} catch ...String str = scnr.next(); System.out.println(str);
数据操作流DataOutputStream/DataInputStream
|
|
合并流SequenceInputStream
用于将两个流合并在一起。
文件压缩流类ZipOutputStream与压缩文件类ZipFile
|
|
网络编程
网络编程基础介绍
- 网络编程的目的是直接或间接地通过网络协议与其他计算机进行通讯。网络编程的两个主要问题,一是如何准确地定位网络上的一台或多台主机,二是找到主机后如何可靠高效地进行数据传输。TCP/IP协议就提供了解决方案,IP层负责网络主机的定位、数据传输的路由;TCP层提供了面向应用的可靠数据传输机制,TCP层是网络编程的主要对象。
- 网络编程模型:当前流行的是C/S结构,即通信双方一作为服务器等待客户提出请求并给予响应,客户端则在需要服务时向服务器提出申请。服务器持续运行,监听网络端口,一旦有客户请求,就会启动一个服务线程来响应客户,在这个过程中依然保持监听端口。
- IP地址:IP网络中每台主机都必须有一个唯一的IP地址,因特网上的IP地址具有全球唯一性。IP是逻辑上的地址,32bit,4byte,点分十进制表示。
- OSI(Open System Interconnection)开发系统互联参考模型将网络的不同功能划分成7层:
物理层 – 数据链路层 – 网络层 – 传输层 – 会话层 – 表示层 – 应用层
各层之间严格单向依赖,下层向上层提供服务。 - OSI各层所用协议:
-> 应用层:远程登陆协议Telnet、文件传输协议FTP、超文本传输协议HTTP、域名服务DNS、简单邮件传输协议SMTP…
-> 传输层:传输控制协议TCP(Transmission Control Protocol,面向连接的可靠传输协议)、用户数据报协议UDP(User Datagram Protocol无连接的不可靠协议)。
-> 网络层:网际协议IP、Internet互联网控制报文协议ICMP、Internet组管理协议IGMP。
-> 物理层:直接传输bit。物理介质分为guided(twisted pair, coax, fiber(single mode/ multimode))和unguided(air).数据链路层
- Node-to-node节点传输,主机、路由都为节点,数据以frame帧的形式在链路上传播,链路分为有线、无线、LAN.
- 提供服务:framing, error detection, error control, medium access control.
- Error detection:处理位错误、包错误
-> 位错误:
parity checking(添个0/1是总1数为奇/偶)
checksum(16bit从左到右依次求和、有进位加到个位)
cyclic redundancy check(给定generator,源bit末尾补上gen.length – 1个0以后作竖式除法)。
-> 包错误:
Stop and wait:死等ACK回复,直到timeout重发。原因:数据丢失、ACK丢失、ACK延迟。
Go back N:将丢失帧及其之后帧一并重传,ACK意指xx号之前的帧已收到并传给上层、期待xx号帧。可加入滑动窗口,SWS定义了发送的未收到ACK帧缓冲范围、RWS = 1足以应付go back N.
Selective repeat:引入NAK,意指xx号帧之前的帧已受到并传给上层、请重传第xx号帧。一旦发送方收到NAK,该丢失帧及其之后帧的timeout都加倍以满足重传。若单单是NAK丢失,不会有大碍,因为timeout机制还是能保证没有ACK的帧会重发一份。落在SW/RW以外的ACK/数据帧直接丢弃。
Selective Acknowledge:??? - PPP = point-to-point protocol
最常用两种:ADSL(ppp over Ethernet) & VPN(ppp tunnel protocol)
PPP分为两步:LCP(link control protocol, 初始化、结束链接)
NCP(network control protocol, 每个网络层协议都会有一个对应的控制协议) - LAN = local area network局域网
共享链路,广播形式传输;范围小,传输速率高。
分为两个子层:
-> MAC(medium access control, 解决接入冲突,如Ethernet, Token Ring, Wifi)
-> LLC(logic link control, 向上层提供Link service) - Error control
MAC层提供的服务。
-> Pure Aloha:一有数据立刻就发,不管数据多长不管信道是否空闲
-> Slotted Aloha:将时间分成一个个时间片,从时间片开头独占则可以发送,有冲突则都以概率p在之后发送
-> CSMA载波监听多路访问:发现信道空闲就发送,分为1监时(持续监听、空闲即发)、无监时(随机隔一阵才监听一次)、p监时(持续监听,空闲时以概率p发送,适用于slotted)。
——————————–以上都无法解决冲突—————————–
-> CSMA/CD带冲突检测的载波监听多路访问:对数据进行分帧,然后1监时持续监听信道,持续96bit都空闲则立即发送,发送过程中均无冲突则ok,有冲突则停止并发送32bit的JAM信号,用二进制指数退避算法延时一段时间后重新开始1监时。二进制指数退避算法就是第一次冲突在0t、1t中选一个时长延时,第二次冲突则0、t、2t、3t中选…十次冲突往后就到2^10=1024 – 1中选。 - Hub与Switch
-> Hub:无存储功能,直接泛洪给其余所有端口,半双工。
-> Switch:有逻辑,可以存储转发给对应MAC地址,全双工
网络层
- Host-to-host
- 提供服务:routing, forwarding.
- Type:circuit switching(FDM, TDM), packet switching(datagram - IP, virtual circuit - ATM).
-> IP(connectionless, best effort).
-> ATM(connection-oriented, QoS). - IP datagram
IP包头20byte+
由于以太网帧有长度限制(数据部分最多1500byte),故可能需要拆分IP包。IP包放入以太网帧的data部分,在协议类型处标明0x8000 IP协议,在MF标记、offset都做好相应记录。 - IP地址
分层的(而MAC地址是flat的)。主机部分不能全为1或全为0.
-> A类网:0.0.0.0 ~ 127.255.F.F [0 7网络 24主机 ]
-> B类网:128.0.0.0 ~ 191.F.F.F [10 14网络 16主机]
-> C类网:192.0.0.0 ~ 223.F.F.F [110 21网络 8主机] - IP控制相关协议
-> ARP(address resolution protocol):将IP这类协议地址转换成LAN中的local地址如MAC。ARP请求帧包含源IP、源MAC、目的IP,等待目的MAC回应。
-> DHCP:动态分配IP。
-> ICMP:显示监测信息,例如超时错误、无法访问目标主机。 - 路由协议RIP、OSPF
-> RIP(routing information protocol):在自治域中每30s向邻居发送路由表,同时接受邻居发过来的路由表,对比更新为最短路。
-> OSPF(open shortest path first):用链路状态让每个路由器都存储整个AS的拓扑和权值。
传输层
- End-to-end. 原理:将进程绑定到指定的IP的指定端口号上,对于发往指定IP指定端口的数据就可以到达对应的进程了。
- 提供服务:可靠传输 + 流量控制。
- UDP(user datagram protocol):无连接,直接一次发送。
- UDP header:8byte,记录了源端口、目的端口、头部和数据长度、校验和。发送时在data前加上Header封成socket,放入下层传输。每个数据报都在64kB以内,多了存不下。
- TCP(transmission control protocol):面向连接,需要建立/释放连接以及互动反馈。采用包交换,一个IP可以向多个端口发起连接,一个端口也可以建立多条连接。源IP、目的IP、源端口、目的端口四者只要一个不同,就认为是新的连接。
- TCP header:20byte(+/-4),记录了源端口、目的端口、数据包序号(保证顺序)、确认序号(指明ACK是谁的)、header长度、6个标志位、发送窗size、校验和etc。
其中的标志位有
-> URG(紧急位,基本不用)、
-> ACK(数据段是否有ACK功能)、
-> PSH(催促接收端尽快上传,基本不用)、
-> RST(重置连接,如在出错时强制中断连接)、
-> SYN(建立连接时同步两端序列号)、
-> FIN(提醒对方我不会再发新的内容了,用于关闭连接)。 - 发送TCP包:需要将data与MSS, maximum segment size比较,若大了就需要拆分。
- 三次握手建立连接:目的是client和server相互确认身份+确认链路完整性(可达且有空闲端口)+ 协商序列号和发送窗口大小。
(1) 客户发起请求:标志位SYN=1,指定seq = x说明客户端初始序列号为x,并告诉服务器自己的RWS窗口剩余大小。
(2) 服务器响应请求:标志位ACK=1,ack字段为x+1表示序列号为x的已经成功收到,SYN=1表示同意跟你连接,再返回seq=y指出服务器的初始序列号,并告诉发送方自己的RWS窗口剩余大小。
(3) 客户确认连接,服务器分配资源:标志位SYN=0,ACK=1,ack字段为y+1表示客户端已经收到了服务端的初始序列号,并继续告诉服务器自己的RWS窗口剩余大小。
-> 一次握手的坏处:client不知自己的请求是否得到了server的接受,无法保证有效连接。
-> 两次握手的坏处:server不知该请求是在信道里阻塞了很久才到达的还是别人恶意伪造占用server端口资源。 - 四次挥手释放连接:
(1) 一方A发起释放请求:标志位FIN=1表示自己不会再发新的数据了,指定seq = x说明A当前序列号为x。
(2) 另一方B收到FIN:标志位ACK=1,ack字段为x+1表示序列号为x的已经成功收到。B会尽快将自己的data处理好。
(3) B回复FIN:B确认data已处置妥当,标志位FIN=1,指定seq=y表示B当前序列号为y。
(4) A收到FIN:标志位ACK=1,ack字段为y+1。此时A会等多一阵子才真正释放连接,而B收到这个包可以立即释放。A还要等一阵子的原因是让ACK到达B,让本次连接的数据从因特网中彻底消失。
应用层
- DNS:Domain Name System
- HTTP:HyperText Tranfer Protocol
基于URL的高层次Java网络编程
- URL(unform resource locator):一致资源定位器,表示因特网上某一资源的地址。
- URL组成:protocol://resourceName,资源名应该是完整地址,包括主机名、端口号、文件名或文件内部的一个引用。
- java.net中实现了类URL。URL编程是基于TCP的应用,要用到下层的socket接口。
创建URL:需要
catch(MalformedURLException e)
1234567891011// public URL(string spec);URL urlBase = new URL("www.xxx.net");// public URL(URL context, String spec);URL index = new URL(urlBase, "index.html");// public URL(String protocol, String host, String file);URL fileOnHost = new URL("http", "www.xxx.net", "/pages/Gamelan.net.html");// public URL(String protocol, String host, int port, String file);URL fileOnPort = new URL("http", "www.xxx.net", 80, "/pages/Gamelan.net.html");解析URL:一个URL对象生成后,它的属性可以通过各种get方法获得,但这些属性都是不能改变的。getProtocol, getHost, getPort, getFile, getRef, getQuery…
从URL读取资源:使用URL对象的openStream方法得到InputStream类对象,便可以从这个连接中读取数据:
123456URL tirc = new URL("http://www.tirc1.cs.tsinghua.edu.cn/");BufferedReader in = new BufferedReader(new InpputStreamReader(tirc.openStream()));String inputLine;while ((inputLine = in.readLine()) != null) {...}向URL发送数据:使用java.net的类URLConnection
12345URL url = new URL("htps://www.javasoft.com/cgi-bin/backwards");URLConnection conn = url.openConnection();DataInputStream dis = new DataInputStream(conn.getInputStream());PrintStream ps = new PrintStream(conn.getInputStream());ps.println("写入服务器的内容");
基于Socket的低层次Java网络编程
- Socket:网络上两个程序通过一个双向的通信连接实现数据交换,这条双向链路的一端称为一个socket。Socket通常用来实现客户端和服务器的连接。
- Socket工作步骤:创建socket -> 打开连接到socket的输入输出流 -> 按照协议对socket进行读写操作 -> 关闭socket。
- Java.net中的类Socket和ServerSocket(待…)
分别用作客户端和服务器。123456789101112131415Socket socket = new Socket("127.0.0.1", 4700);BufferedReader sin = new BufferedReader(new InputStreamReader(System.in));PrintWriter os = new PrintWriter(socket.getOutputStream());BufferedReader is = new BufferedReader(new InputStreamReader(socket.getInputStream()));String readline = sin.readLine();while (!readline.equals("exit")) {os.println(readline);os.flush();readline = sin.readLine();}ServerSocket sv = null;sv = new ServerSocket(4700);// 服务端待补充...
表示层
定义数据格式及加密。
会话层
定义如何开始、控制和结束一个会话。对多个双向消息的控制和管理,让表示层看到的数据是连续的。
设计模式
设计模式六大原则
- 单一职责:每个类值负责一项职责,不要引入多于一个导致类变更的原因。只有逻辑足够简单,才可以在代码级别上违反单一职责的原则(引入新的逻辑,如if分支、for循环);只有类的方法数量足够少,才能在方法级别上违反单一职责原则(加入与原方法并列的新方法)。这个并不是面向对象编程特有的,只要模块化设计都必须遵循单一职责原则。
- 里氏替换:所有引用基类的地方必须都能透明地使用其子类的对象,当类B继承类A时除添加新的方法完成新的功能之外,尽量不要重写覆盖(重载也不要)A的方法。这是为了限制面向对象编程时由于继承带来的耦合性,特别是运用多态比较频繁的时候,重写的方法出错几率更大。
- 依赖倒置:高层模块不应依赖低层模块,它们都应依赖更高层的抽象(接口),核心是面向接口编程。相对于细节的多变性,抽象的东西要稳定得多。以抽象为基础搭建起来的架构比以细节为基础搭建起来的架构稳定得多。在java中,抽象指的就是接口或抽象类,细节就是具体的实现类,使用接口或抽象类的目的是指定好规范和契约,将展现细节的任务交给实现类去完成。所以底层模块尽量用抽象类或接口、变量的声明类型尽量是抽象类或接口、同时继承时遵循里氏替换原则。
接口隔离:一个类对另一个类的依赖应该建立在最小接口上。核心思想是建立单一的接口,尽量细化接口使每个接口方法尽量少。通过分散多个接口可以预防外来变更的扩散(所有实现接口的类都要实现全部接口中的方法)。例如interface Itf 有五个方法method1~5。class A中的三个方法depend1~3(Itf i)分别用到了Itf接口的前三个方法1~3,class B实现Itf接口并在使用时传入A的方法的参数列表。如此一来,B不得不实现I中的4、5方法,尽管他们压根不用(因为A不用)。正确的做法就是将I拆分成前三个和后两个,这样针对类A的三个方法就可以构造一个只实现Itf中1~3方法的类B了。
12345678910111213141516171819202122232425262728293031323334353637383940414243interface Itf {public void method1();public void method2();public void method3();public void method4();public void method5();}class A {public void depend1(Itf i) {i.method1();}public void depend2(Itf i) {i.method2();}public void depend3(Itf i) {i.method3();}}class B implements Itf {public void method1() {System.out.println("B implements method1 in Itf");}public void method2() {System.out.println("B implements method2 in Itf");}public void method3() {System.out.println("B implements method3 in Itf");}// 由于实现了接口的非抽象类必须完全实现所有接口函数,所以即使A不会用method4和5也要实现一波,即使里面什么也不做public void method4() {}public void method5() {}}public class Client {public static void main(String[] args) {A a = new A();a.depend1(new B());a.depend2(new B());a.depend3(new B());}}最少知道法则:一个对象应对其他对象保持最少的了解。软件编程的原则是低耦合、高内聚。对象应该只与直接的朋友通信,直接的朋友指的是成员变量、方法参数、方法返回值中的类,陌生的类不应以局部变量出现在类的内部。但是这意味着需要通过中介类来实现陌生类的通信,这可能会导致系统变得复杂。
- 开闭原则:一个软件实体如类、模块、函数应该对扩展开放、对修改关闭。当软件需要修改时,尽量通过扩展软件实体的行为来实现变化而不是修改已有的代码来实现变化。用抽象构建框架,用实现拓展细节。
Design Pattern revisit(18653 Software Architecture and Design)
Creational Patterns
提供了一种在创建对象的同时隐藏创建逻辑的方式,而不是使用 new 运算符直接实例化对象。这使得程序在判断针对某个给定实例需要创建哪些对象时更加灵活。
- factory: 创建对象时不会向client暴露创建的逻辑,通过一个共同的接口让client决定创建那种具体子类。
- abstract factory: 用一个抽象工厂作为「其他工厂的工厂」,具体的工厂extends该超级工厂,再通过接口创建具体的类。
- singleton: 通过一个类创建自己的对象,同时保证只有一个对象存在于系统中。
- Builder: 产生一个复杂对象时通过多个更简单的对象一步步生成。
- Prototype: 直接创建对象比较costly(如一顿复杂的数据库调用才能创建出来),在创建duplicate对象时可以通过一个原型接口创建该对象的clone返回。
Structural Patterns
关注类和对象的组合。继承的概念被用来组合接口和定义组合对象获得新功能的方式。
- Adapter: 使用一个类作为两个不兼容接口的桥梁,创建真正可以执行该操作的对象并调用对应的方法。
- Bridge: 将抽象化与实现化解耦,提供二者之间桥接结构的借口,使实体类的功能独立于接口实现类。
- Filter(Criteria): 使用不同的标准来过滤一组对象,通过逻辑运算(AND OR)以解耦的方式把它们连接起来,结合多个标准来获得单一标准。
- Composite: 整体部分模式,把一组相似的对象当作单一的对象对待,想象一个树形结构来组合对象。
- Decorator: 向一个现有对象添加新的功能,动态地给对象添加额外的职责同时不改变它的结构。
- Facade: 隐藏系统的复杂性,只给client暴露一个访问的接口(不一定非得是interface,实体类也可),提供了简化的client方法和对系统类方法的调用。
- Flyweight: 减少创建对象的数量,重用现有的同类对象,找不到再新建(类似cache)。
- Proxy: 为其他对象提供一个代理来控制对某对象的访问(类似快捷方式)。
Behavioral Patterns
关注对象之间的通信。
- Chain of responsibility: 为request创建了handler的链,如果当前对象不能处理该请求就通过next传递到下一个。
- Command: 请求以命令的形式包裹在对象中,再将命令对象传给接受者进行相应的操作。
- Interpreter: 实现一个表达式接口,解释特定的上下文如语言、表达式等。
- Iterator: 迭代遍历。
- Mediator: 提供一个中介类来处理不同类之间的通信,降低多个对象/类之间的通信复杂性。
- Memento: 保存一个对象的中间状态,以便在适当的时候恢复对象。
- Observer: 存在一对多的自动推送关系时使用。
- State: 类的行为基于其状态而改变,需要创建不同状态的对象和context对象来随状态对象改变而改变行为,context作为参数传入各种State,由state对象改变context的状态并执行行为。
- Strategy: 类的行为或算法可以在运行时改变,需要创建各种策略的对象和context对象来随策略对象改变而改变行为,策略的实现类作为参数传入context,由context执行行为。
- Template: 一个抽象类作为模版,子类可以重写其中的方法,client通过模版提供的方法进行调用。
- Visitor: 使用visitor类,改变元素类的执行算法。基础类对象接受visitor,同时将自身引用传给visitor供其visit。
数据库
Catelog/database数据库
采用多数据库的方式来保存不同类别的数据。这样『多数据库』管理的模式可以便于对各个catelog进行个性化管理,同时也可避免表名命名冲突,且可指定各catelog的各用户接入许可使安全性更高。
- Table表:将数据库内的数据分类管理,即将不同类型的资料放到不同的”区域”中,这些区域就是表。
- Column列:又称field字段,给一个表内的物品规定好标签格式,用于统一地显示数据的特性。
- DataType数据类型:对每一列填入数据给出规定,例如类型、字数等,可节约空间占用提高访问效率、确定对数据进行操作的正确处理(字符串的+与数值的+)。
- Row/record记录:一行数据就代表了一个表中某个对象的资料。表其实就是行和列的二维表。
- PrimaryKey主键:唯一标识一行记录的ID.
- Index索引:为某一列建立索引相当于自定义排序,这样之后搜索匹配时可以快速定位到第一个匹配项的位置、大大减少扫描量。当然,索引会占据一定的磁盘空间,且降低了数据插入和删除的速度(因为每次插入删除都必须更新索引),特别是当表拥有的索引很多时。
- 表关联:将表中某些field的信息替换成其他表的ID。当某些信息在表中重复多次形成冗余,会无谓地占据空间,且相同数据录入同样还要重复输入这些信息,在日后维护数据正确性时不得不把所有表中相关数据都更新一遍。若我们为这些数据新建一个表,则可以通过编号(主键)唯一标识一个对象,则对应字段都可以用编号的方式关联到这个专门新建的表了。通过表关联可以使数据不再孤立、表达复杂的数据关系。
SQL(structured query language)
高级非过程化语言,在高层数据结构上工作,具有不同底层结构的数据库系统可以使用相同的SQL语言作为数据输入和管理的接口。
table
创建数据表:可设置非空、默认值,指定主键/复合主键,定义外键。
12345678910CREATE TABLE tableName(fieldName1 type(maxLength) NOT NULL,fieldName2 type(maxLength) NOT NULL,fieldName3 type(maxLength) DEFAULT defaultValue,…PRIMARY KEY(fieldName1), // 放在字段定义之后,主键必须设置非空约束// PRIMARY KEY(fieldName1, fieldName2), 联合主键,慎用FOREIGN KEY(fieldName3) REFERENCES anotherTableName(fieldName));定义外键:外键是体现关系数据库中”关系”二字的关键,使用外键可以把互相独立的表关联起来,即在某字段引用其他表的主键作为值。
修改已有数据表
12ALTER TABLE tableName ADD newFieldName type(maxLength);ALTER TABLE tableName DROP oldFieldName;删除数据表
DROP TABLE tableName;
注意若存在外键关联关系,被引用的数据表必须等到引用的表删除后才能删除,否则会破坏外联关系导致已有的表出问题。
Data manipulation
数据的检索/排序/整理
ORDER BY语句必须放在SELECT完整语句末尾;当前面的规则得出字段值相等时,会根据后面的规则进一步排序。
GROUPBY语句可以进行分组,将指定fieldName相等的条目划分成一组。123456789101112131415161718192021222324-- 直接取出所有数据。SELECT * FROM tableName;-- 取给定列且若重复出现则只取一个SELECT DISTINCT colName, colName2 FROM tableName;-- 只取1/多个field字段下的所有数据。SELECT fieldname1, fieldname2 FROM tableName;-- 给列起别名。SELECT fieldName1 AS Name, fieldName2 AS Salary FROM tableName;-- 按条件过滤。SELECT * FROM tableName WHERE fieldName1 < 2000 OR fieldName2 > 20;-- 先按用户名升序,相同则按年龄降序SELECT * FROM tableNameORDER BY username ASC, age DESC;-- 根据column_name进行分组合并,通常用于统计特定id的sumSELECT column_name, aggregate_function(column_name)FROM table_nameWHERE column_name operator valueGROUP BY column_name;插入数据
12INSERT INTO tableName (colName1, colName2, colName3)VALUES (value1, value2, value3);以上的插入语句不一定要把所有字段都填写完整,只要不要求非NULL就可以留空。
主键值必须唯一,当主键字段的值已经出现过,则插入会报异常。
外键值必须已存在对应的表、且目标表中存在该外键值。更新数据
1234567891011UPDATE table_nameSET column1 = value1, column2 = value2,...WHERE some_column = some_value;```其中WHERE语句可以加入逻辑连接词如OR、NOT。主键值必须唯一,当更新主键字段的值已经出现过,则更新会报异常。外键值必须已存在对应的表、且目标表中存在该外键值。* 删除数据```SQLDELETE FROM tableNameWHERE fieldAge > 20;删除表中的数据,完成后表为空。
数据汇总:提供了MAX, MIN, AVG, SUM, COUNT等函数。
1234567891011121314151617-- 求最大SELECT MAX(fieldName1) AS Max_SalaryFROM tableNameWHERE fieldName2 < 25;-- 求均值SELECT AVG(fieldName1)FROM tableNameWHERE fieldName2 < 25;-- 求和SELECT SUM(fieldName1)FROM tableName;-- 这里的COUNT可以用*表示全部数据有多少条目,而传入某些字段则会忽略该字段为NULL的条目。SELECT COUNT(*), COUNT(fieldName2)FROM tableName;通配符过滤LIKE:可以模糊检索字符串
12345-- 指定了该字段的值共五位,第一位任意SELECT * FROM tableName WHERE fieldName1 LIKE "_eery";-- "k%": 以k开头,长度任意的字符串。-- "%n%":含有n的字符串,长度不限,n的位置不限。-- "%n_":含有n的字符串,长度不限,n的位置一定在倒数第二位。NULL的检索
12SELECT * FROM tableNameWHERE fieldName1 IS NOT NULL AND fieldName2 IS NULL;反义运算符
和一般编程不同,这个可以直接在<、>前加个!. 特别地,<>表示不等于。- 多值检测
为了简化OR,可以用IN语句。WHERE fieldname IN (a, b, c); - SQL JOIN
-> INNER JOIN:如果表中有至少一个匹配,则返回行
-> LEFT JOIN:即使右表中没有匹配,也从左表返回所有的行
-> RIGHT JOIN:即使左表中没有匹配,也从右表返回所有的行
-> FULL JOIN:只要其中一个表中存在匹配,则返回行
Java 8 新特性
Stream
Stream API用于处理Collection中的object,提供可pipelined的方法(若干个intermediate方法和一个terminal方法)产生结果而不会改变原数据结构中的值。
Intermediate Operations
map: 映射每个元素到对应的结果。
123// 获取对应的平方数List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);List<Integer> squaresList = numbers.stream().map(i -> i * i).distinct().collect(Collectors.toList());filter: 设定条件只保留满足条件的元素。
123// 获取空字符串的数量List<String>strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");int count = strings.stream().filter(string -> string.isEmpty()).count();sorted: 对流进行排序。
- 此外还有一些类似于SQL的操作如count, limit。
Terminal Operations
collect: 将操作结果收集并返回,可与Collectors类结合使用。
123List<String>strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");List<String> filtered = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.toList());String mergedString = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.joining(", "));forEach: 遍历stream中的每个元素。
12Random random = new Random();random.ints().limit(10).forEach(System.out::println); // 这有一个Java8特性,将方法引用作为参数reduce: 将stream元素聚拢成为一个结果。
1234// 求数组中偶数之和List number = Arrays.asList(2,3,4,5);int even = (int) number.stream().filter(x-> ((int)x % 2 == 0)).reduce(0,(ans,i)-> ((int)ans + (int)i));System.out.println(even);