Java知识点小记(零碎版 超速更新)

声明

全部知识点以及部分图片来自中软国际上课时用的PPT。

堆、栈、常量池、方法区

这里写图片描述

基本数据类型和引用类型的区别

基本数据类型存储在栈中,引用类型存储在堆中;

  • 在函数(方法)中定义的基本数据类型变量存储在栈中;
  • 引用类型实例的引用(reference)也是存储在栈中;
  • 引用类型实例的成员变量,存储在堆中;

这里写图片描述

每种具体类型的长度及特点

这里写图片描述

基本数据类型的显式和隐式转换

从表示范围小的类型转换为表示范围大的类型,可以直接转换,称为隐式转换。

从表示范围大的类型转换为表示范围小的类型,需要强制转换,称为显式转换。

注意

虽然类型之间可以进行强制的隐式转换,但是也需要有一定的前数值类型和boolean类型之间就不能转换,强制也不可以。

小数默认为double类型;使用f或F后缀可以表示该小数是float型

引用类型

引用类型可以使用==、!=进行比较,比较的是引用类型的地址,不是内容。

引用类型不能使用>、>=、<=、<进行比较。

一个字符串是null,与一个字符串是””(空字符),不一样,空字符也是字符,而null只在栈中有定义,在堆中无内容。

自动装箱拆箱

基本数据类型转换为包装器类型,称为装箱(boxing)。

包装器类型转换为基本数据类型,称为拆箱(unboxing)。

在自动装箱拆箱过程中,Java使用到了常量池。

在自动装箱拆箱过程中,只有数值是byte范围内的时候,才使用到常量 池,否则都是分配新的内存空间;

字符串常量池

字符串可以用两种方式赋值有一个非常重要的特征,即不可变性(immutable):一旦一个字符串被创建后,它的值就不能被修改

String s1="Hello"; 
s1="World";

这里写图片描述

并不是把Hello改为了World,而是重新分配空间存储World。

为了能够重用这些不变的字符串,Java使用了字符串常量池。

凡是用=直接赋值的方式得到的字符串,都存储在常量池中。相同的变量共用一个具体字符串。

使用new创建的字符串不适用常量池,每次都分配新的内存空间。

String s2="Hello"; 
String s3="Hello"; 
String s4=new String("Hello"); 
String s5=new String("Hello");

这里写图片描述

StringBuffer

字符串缓冲区,所表示的也是一个字符序列。

这个类型必须用new创建对象,和String相反,它是可变的类,即字符串的值可以被改变。

StringBuilder

与StringBuffer兼容,但是不保证线程 同步。

单线程的情况下比StringBuffer高效,必须使 用new关键字。

数组概念和作用

数组中可以存储多个数据,但是这些数据的类型必须相同

Java的数组特性

Java的数组是引用类型。

Java的数组长度一经确定不能改变。

数组在内存中是连续分配,所以读取速度快 。

一维数组的声明形式

数组元素类型[ ] 变量名称; 
或 
数组元素类型 变量名称[ ] ;

例如: 
int[] a; 或 int a[]; 
String[] s; 或 String s[];

一维数组的初始化

第一种:数组元素类型[ ] 变量名称=new 数组元素类型[数组长度];
第二种:数组元素类型[ ] 变量名称=new 数组元素类型[]{用逗号隔开元素的具体值};
第三种:数组元素类型[ ] 变量名称= {用逗号隔开元素的具体值}; 

数组的首地址存放在栈中,内容存放在堆中。

数组的长度

在创建数组的时候,一定要确定数组的长度。

数组的长度将在初始化数组元素的时候同时初始化到内存中。

使用 数组变量名.length 可以返回数组的长度。

数组的遍历

int[] a=new int[]{1,2,10}; 
//使用for循环遍历 
for(int i=0;i<a.length;i++){ 
    System.out.println(a[i]); 
} 
//使用增强for循环遍历 
for(int x:a){ 
    System.out.println(x); 
}

数组排序

Java API中有一个类 Arrays,定义了大量的sort方法,可以对数组中元素进行排序。

Arrays.sort(a); //将数组a的元素 升序排序

基本数据类型或字符串类型的多维数组

如果同时确定一维和二维的长度,则表示数组的元素是等长的一维数组。

如果数组元素不是等长的一维数组,可以不指定二维长度。

数组元素类型[ ][ ] 变量名称=new 数组元素类型[一维长度] [二维长度];

continue和break

可以在for、while、do前用合法标识符加标号。

break 标号;语句终止指定的循环,或者用 continue标号;语句进入下一个循环。

loop1:   for(int i=0;i<5;i++){
    loop2:  for(int j=0;j<6;j++){ 
        //some code
        break loop1;
        //或者 continue loop1;
    }
}

面向对象最重要的三大特征是:封装、继承、多态。

封装

封装是与对象有关的一个重要概念 。

形式:将数据和行为组合在一起,并对对象的使用者隐藏数据的实现方式 。

属性由变量表示,属性名称由类的每个对象共享 。

每个特定的对象都有一组特定的实例属性值,这些值的集合就是这个对象 的当前状态,只要向对象发送一个消息,它的状态就有可能发生改变。

封装的特性能够让服务提供者把它服务的细节隐藏掉,你只需要提交请求 与传递它需要的参数,它就会给你返回结果,而这个结果是如何产生的, 经过了多少复杂运算,经过多少次数据读取,你都不用管,只要它给你结 果就好了。

封装使得对代码的修改更加安全和容易,将代码分成了一个个相对独立的单元, 对代码访问控制得越严格,日后你对代码修改的自由就越大 .

能很好的使用别人的类,而不必关心其内部逻辑是如何实现的,让软件协同开 发的难度大大降低。

类的基础声明形式

[访问权限修饰符] [修饰符] class 类名

类的命名规范

类的名字由大写字母开头而单词中的其他字母均为小写。

如果类名称由多 个单词组成,则每个单词的首字母均应为大写,把这些单词连接在一起, 即不要使用下划线分割单词,例如:OrderList 。

如果类名称中包含单词缩写,则这个所写词的每个字母均应大写,如: XMLExample 。

由于类是设计用来代表对象的,所以在命名类时应尽量选择名词。

类成员初始化

不管在任何地方,引用类型都需要初始化后才可使用,因为引用类型的初始值为 null,代表不具备存储数据的内存空间,直接使用会造成程序运行异常。

对象的作用域

由new创建的对象,只要需要,就会一直保留下去。

调用类的成员属性

使用成员运算符(.)来访问成员。

student.age=18

方法的基本声明形式

[访问控制][方法修饰] 返回类型 方法名称(参数1,参数2,....){
    …(statements;)    //方法体:方法的内
}

方法名和参数列表(它们合起来被称为“方法签名”或者“参数签名”)唯一地标识出某个方 法

方法中可以调用方法,不可以在方法内部定义方法。

调用类方法

成员方法也使用成员运算符(.)来调用。

同一个类中的方法在本对象中调用其他方法直接使用方法名(static方法 后续探讨)。

方法命名规范

方法的名字的第一个单词应以小写字母作为开头,后面的单词则用大写字 。

方法的名字的第一个单词应以小写字母作为开头,后面的单词则用大写字 。

方法参数的传值特性

Java中的参数只有值传递,即都是形参。

传统印象中基本数据类型和字符串传参时是值传递,其他对象传参是是引 用传递的思想是错误的。

方法体中的代码操作的是形参变量,和实参无关,只不过由于需要借助于 实参的数据值,因此在执行方法第一条语句之前,隐式按照参数的位置关 系利用实参对相应位置的实参进行了赋值操作。

public class ArgumentCallByValueTest{
    void changeArgument (StringBuilder src){
        src.append("@Java");
        src = new StringBuilder("Hello");
        src.append("World");
    }
    public static void main(String[] args){
        StringBuilder strBuilder = new StringBuilder("Chinasofty");
        ArgumentCallByValueTest argTest = new ArgumentCallByValueTest();
        System.out.printIn(strBUilder);
    }
}   

运行结果为:Chinasofty@Java

这里写图片描述

当方法中的形参在没有调用不可变API之前,形参的任何改变都将影响实 参的状态,而当形参执行了任何不可变API之后,形参和实参之间就断开 了这种状态联系。

可变参数

Java把可变参数当做数组处理 。

可变参数必须位于最后一项。

当可变参数个数多余一个时,必将有一个不是最后 一项,所以只支持有一个可变参数。

因为参数个数不定,所以当其后边还有相同 类型参数时,java无法区分传入的参数属于前一个可变参数还是后边的参数,所以 只能让可变参数位于最后一项。

可变参数用…代替标识, …位于变量类型和变量名之间,前后有无空格都可以。

用可变参数的方法时,编译器为该可变参数隐含创建一个数组,在方法体中以数 组的形式访问可变参数。

方法重载

方法重载即指同一个类中多个方法可以享有相同的名字。但是这些方法的 参数类型列表必须不同,或者是参数个数不同,或者是参数类型不同,或 者是参数类型的排列顺序不同 。

当使用基本数据类型作为参数时,如果不能精确匹配到自身的数据类型且 实参的范围小于形参,则将自动匹配离形参最近的方法声明并对形参进行 自动的类型转换。

如果实参范围大于形参,则需要显示对实参进行类型转换。

构造方法

构造方法是与类同名的方法。

没返回值,也不能写void 。

主要作用是完成新建对象的初始化工作 。

一般不能显式地直接调用,而是用new来调用(后面会存在使用this/super调用)。

不能使用修饰符,包括static、final、abstract。

每个对象在生成时都必须执行构造方法,而且只能执行一次 。

由于Java要求每个类都必须要提供构造方法来构建对象,如 果程序员认为编写的类无需特殊初始化操作而没有提供任何一个构造方法 的话,Java会自动为该类提供一个默认的构造方法 。

格式:

[访问权限修饰符]类名(参数列表){
    方法体
}

使用构造方法创建对象

对象的声明和初始化的结 构准确的说应该是

类名 引用变量名 =  new  类的构造函数(构造方法参数列表);

一旦显式地定义了构造方法,默认构造方法自动消失,即便显式定义的构 造方法不是无参的。

构造方法是一种特殊的方法,它也能重载。

构造函数的重载是指同一个类中存在着若干个具有不同参数列表的构造函 数,和普通的方法一样,将根据new运算符后面的参数类型列表判定使用的构造方法版本。

this关键字的作用

它指向了调用该方法的对象自身 。

在对象内部直接使用this作为自身的引用。

对于构造方法而言,this还有一个特殊作用:那就是在构造方法中调用本 类的其他构造方法 。

如果有一个类带有几个构造函数,那么也许会想复制其中一个构造函数的 某方面效果到另一个构造函数中,可以通过使用关键字this作为一个方法 调用来达到这个目的。

如果出现这种情况,在任何构造方法中this调用必须是第一个语句。

将this用于传递本对象引用句柄的用法也很常见。

由于this的这个特性,以后经常使用它来实现对象方法的链式调用:

public class Leaf{
    private int i = 0;

    Leaf increment(){
        i++;
        return this;
    }

    void print(){
        System.out.printIn("i = " + i);
    }

    public static void main(String[] args){
        Leaf x =new Leaf();
        x.increment().increment().increment().print();
    }
}

结果:i = 3。

类初始化代码块(static)

类初始化代码块(static)

如 果我们能够在虚拟机加载某一个类时即可以触发某一个操作,那么我们就 能够完成一些更为通用的信息初始化工作,这对于某一些功能组件显得尤 为突出(如JDBC)。

类初始化代码块在类中编写,是在类中独立于类成员的static语句块,可 以有多个,位置可以随便放,它不在任何的方法体内,基础结构如下。

static{
    //被static{}框定的代码段将在该类被加载时自动 执行
}

当一个类中有多个static{}的时候,按照static{}的定义顺序,从前往后执行 。

先执行完static{}语句块的内容,才会执行调用语句。

一般 情况下整个生命周期中,类都会只加载一次,又因为static{}是伴随类加载 执行的,所以,不管new多少次对象实例,static{}都只执行一次

static代码块的执行时机:

  • 用Class.forName(类名)显式加载的时候(反射、JDBC时详细讲解)
  • new或反射实例化一个类的对象时候
  • 调用类的static方法的时候(后续详细讲解)
  • 调用类的static变量的时候(后续详细讲解)

调用类的静态常量(后续详细讲解)的时候,是不会加载类的,即不会执行static{}语句块当 访问类的静态常量时,如果编译器可以计算出常量的值,则不会加载类,否则会加载类 。

用Class.forName()形式的时候,也可以自己设定要不要加载类,如将Class.forName(“Test”)改 为 Class.forName(“Test”,false,StaticBlockTest.class.getClassLoader()),你会发现Test没有被加 载,static{}没有被执行。

static代码块不能初始化类的普通成员变量,只能初始化static变量,其中 也无法使用this/super,因为执行代码段时还未构建对象。

如果静态变量在定义的时候就赋给了初值(如 static int x=100),那么赋值 操作也是在类加载的时候完成的,并且当一个类中既有static{}又有static 变量的时候,同样遵循“先定义先执行”的原则。

实例初始化代码块

实例初始化代 码块在类初始化代码块的基础上去掉了static关键字。

初始化块虽然也是Java类的一部分,但它没有名字,也就没有标识,因此无法 通过类、对象来调用初始化块。初始化块只在创建Java对象时隐式执行,而且 在执行构造器之前执行。

如果有一段初始化代码对所有对象都相同,且无须接收任何参数,就可以把这 段初始化代码处理代码提取到初始化块中。

和static代码块不同,每次实例化对象时均会执行一次实例初始化代码块, 且示例初始化代码块中可以访问成员属性,并能够使用this引用。

初始化代码块和构造方法的运行顺序

类初始化块
实例初始化块
构造方法

JDK8的 :: 关键字

在JDK8中,类的普通方法及构造方法能够通过::关键字被其他类的方法引 用。

如果在一个包中,一个类想要使用本包中的另一个类,那么该包名可以省略。

包和子包之间不存在继承关系,只要两个类不直接在同一个文件中 即认为位于不同的包,因此*号只能包含本包中的类而不能包含子包中的 类。

编写Java代码后在命令行通过javac进行编译时,如果没有特殊说明,即 便代码中存在package语句声明了代码所在的包,也会直接在当前文件夹 中直接生成.class文件(不会生成包路径层次结构)。

编译时添加-d参数,可以明确编译以后的结果保存的位置,并且会在该 位置中自动生成对应的包路径结构。

类的访问控制符

顶层类的访问级别

  • 默认的(不提供访问控制符):仅可被同包的其他代码访问

  • public: 可以被任何代码访问

Java中的类根据是否提供public关键字来划分为两种权限:公开的类能够被所有的其 他类访问,而非公开的类(default)只能被同一个包中的其他类访问

类成员的访问级别有四种

private
default(不使用default关键字,和类的default类似,不提供修饰符即为默认权限)
protected
public

public(公开)

任何其它类对象,只要可以看到这个类的话,那么它就可以存取变量的数据,或 使用方法。

不能单纯以成员访问控制符确定一个成员是否能够访问,如果类本身不能 被访问,那么即便成员为public公开权限,也是不能被访问的。

protected(受保护)

如果一个类中变量或方法有修饰字protected,同一类,同一包可使用。不同包的 类要使用,必须是该类的子类(继承关系,后续详细介绍)。

需要注意的是,即便在非同包的子类中,也只能通过直接调用继承下来的成员的 方式访问protected成员,而不能使用父类的引用进行调用。
这里写图片描述
这里写图片描述

default(缺省):

在同一程序包中出现的类才可以访问。

即便是子类不再同一个包中也不能访问。

private(私有)

只能被同一个类的访问。
这里写图片描述

封装的实现

修改属性的可见性。

限制访问权限。

设置属性的读取方法。

继承的意义

复用代码是Java众多引人注目的功能之一。

继承是java面向对象编程技术的一块基石,因为它允许创建分等级层次的 类 。

  • 继承是能自动传播代码和重用代码的有力工具
  • 继承能够在某些比较一般的类的基础上建造、建立和扩充新类
  • 能减少代码和数据的重复冗余度,并通过增强一致性来减少模块间的接口 和界面,从而增强了程序的可维护性
  • 能清晰地体现出类与类之间的层次结构关系
  • 继承是单方向的,即派生类可以继承和访问基类中的成员,但基类则无法 访问派生类中的成员
  • 在Java中只允许单一继承方式,即一个派生类只能继承于一个基类,而不 能象C++中派生类继承于多个基类的多重继承方式

构造方法与继承

有一样是子类继承不了的:构造方法。

对于构造方法而言,它只能够被调用,而不能被继承。

当构建子类对象时会优先隐式自动调用父类的无参构造方法,而且这个构 建调用过程是从父类“向外”递归扩散的,也就是从父类开始向子类一级一 级地完成构建,即如果C继承自B,而B继承自A,那么构建C的对象时, 会先调用A的构造方法,然后调用B的构造方法,最后调用C的构造方法, 以此类推。

对于继承而已,子类会默认调用父类的无参构造方法,也 就是说子类必须能够访问父类的一个构造方法,并且一定会调用,所以父类不能只有private修饰的构造方法

super

如果没有无参的父类构造方法,子类必须要显示的调用父类的构造方法,而且 必须是在子类构造器中做的第一件事

通过super关键字可以在子类构造方法中显式调用父类的构造方法,该调用必 须位于子类构造方法的第一行

子类覆盖

发生方法覆盖的两个方法的方法名、参数列表必须完全一致(子类重写父类的方 法) ,方法返回值如果是基本数据类型,则返回值应该保持一致,如果返回值 是类,则子类覆盖方法的返回值必须是父类方法返回值或其的子类(协变返回 类型)

子类抛出的异常不能超过父类相应方法抛出的异常(子类异常不能大于父类异 常) (异常处理章节详细介绍)

子类方法的访问级别不能低于父类相应方法的访问级别(子类访问级别不能低于 父类访问级别

如果在子类覆盖的方法里或其他地方需要明确使用父类声明的方法版本, 也可以使用之前遇到的super关键字显示调用

事实上,子类中也能够声明和父类中同名的成员变量,此时在子类中通过 变量名访问,使用的是子类自己定义的成员,也可以使用super来显示调 用父类成员

虽然能通过super.function()的方式调用父类中声明的function 方法,但super和this不同,它不是一个真正意义上的引用,因此不能将 其作为参数传递给其他调用者

return super是错的。

多态

多态的 主要作用适用于消除类型之间的耦合关系

Java的多态就是指程序中定义的引用变量所指向的具体类型和通过该引用 变量发出的方法调用在编译时并不确定,而是在程序运行期间才确定,即 一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用 到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程 序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变 量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变, 即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以 选择多个运行状态,这就是多态性。

对象向上造型

父类的引用(栈中)指向子类的对象(堆中)。

人 person = new 男人();

其中的“人”为编译器类型,“男人”为运行期类型。

引用变量在编译阶段只能调用其编译时类型所具有的方法,但运行时则执 行它运行时类型所具有的方法 。

引用只能调用编译期类型里包含的成员 。

通过Object p =new Persion()代码定义一个变量p,则这个p只能调用Object类的方法,而不能调用Persion类里定义的方法

对象在满足条件的情况下也能进行向下造型,即显式的将父类引用指向的 对象转换为子类类型 .

说白了就是强制类型转换。

人 person = new 男人();
男人 man = (男人)person;

对象的运行期类型是男人 本身,可以 造型。

多态环境下对属性和方法的调用特点

Java代码中的数据和行为(变量和方法)在进行绑定(即通过对象调用成 员变量或方法时究竟调用哪个版本,如覆盖后的方法)的时候划分为两种 类型:

静态绑定
动态绑定

静态绑定发生在编译时期,动态绑定发生在运行时

类的成员变量(属性)都是静态绑定的(编译时),也就是说,类中声明 的成员变量不能被子类中的同名属性覆盖,通过该类的引用调用成员,始 终调用该类自身中声明的属性(即始终调用编译期类型中的属性)

对于Java中的方法而言,除了final(后续详细介绍),static(后续详细 介绍),private和构造方法是静态绑定外,其他的方法全部为动态绑定 。

多态参数的使用

如果将方法的形参参数声明为父类类型,结合前序章节介绍的方法参数的 功能(即调用方法代码前会隐式执行形参和实参之间的赋值操作),由于 子类的对象赋值给父类的引用是合法的,那么在调用方法时,实参就可以 是以形参类型为根的继承树中的任意类型 。

此时形参对应的运行期类型和传进来的实参运行期类型保持一致这里写图片描述

public class Super1{
    public void foo(){
        System.out.println("父类");
    }
    public void test(Super1 arg){
        arg.foo();
    } 
    public static void main(String[] args) {
        Super1 test = new Son();
        test.foo();
        test.test(test);
    }
}
class Son extends Super1{
    public void foo(){
        System.out.println("子类");
    }
}

instanceof运算符

运算符instanceof用来判断对象是否属于某个类的实。

对象 instanceof 类

该表达式为一个boolean表达式,如果对象的类型是后面提供的类或其子 类,则返回true,反之返回false。

多态存在的三个必要条件

继承

重写覆盖

对象向上造型-父类引用指向子类对象

多态的好处

可替换性(substitutability)

多态对已存在代码具有可替换性

可扩充性(extensibility)

多态对代码具有可扩充性。增加新的子类不影响已存在类的多态性、继承性,以及其他特性的运行和操作。实际上新 加子类更容易获得多态功能

接口性(interface-ability)

多态是超类通过方法签名,向子类提供了一个共同接口,由子类来完善或者覆盖它而实现的

灵活性(flexibility)

它在应用中体现了灵活多样的操作,提高了使用效率

简化性(simplicity)

多态简化对应用软件的代码编写和修改过程,尤其在处理大量对象的运算和操作时,这个特点尤为突出和重要。

抽象(abstract)的作用

抽象类主要用来进行类型隐藏;也就是使用抽象的类型来编程, 但是具体运行时就可以使用具体类型 。

抽象类,抽象方法,在软件开发过程中都是设计层面的概念。也就是说, 设计人员会设计出抽象类,抽象方法,程序员都是来继承这些抽象类并覆 盖抽象方法,实现具体功能 。

理想的继承树应该是:所有叶子都是具体类,而所有的树枝都是抽象类。 在实际中当然不可能完全做到,但是应尽可能向此目标靠拢(接口继承)。

抽象类不可以直接实例化,只可以用来继承作为其他类的父类存在 。

抽象类不可以直接实例化,只可以用来继承作为其他类的父类存在 。

如果抽象类的派生子类没有实现其中的所有抽象方法,那么该派生子类仍 然是抽象类,只能用于继承,而不能实例化,但可以有构造函数(用于帮 助子类快速初始化共有属性)。

抽象方法

抽象方法同样用abstract说明,抽象方法没有方法体,只有方法签名 。

构造方法和final、static方法(后续讲解)不可以修饰为abstract。

抽象方法必须位于抽象类中(后续会介绍接口也能作为抽象方法的容器)。

如果子类继承抽象类后没有完全实现要求实现的 抽象方法,编译器将会给出对应的提示。

两个解决问题的方式:

  1. 实现抽象方法
  2. 将本类也定义为abstract抽象类

常量

如果将某个变量修饰为final,那么该变量就成为常量。
[访问权限] final 数据类型 常量名 = 值

常量在声明时必须初始化,声明之后不能对其进行二次赋值,其后任何试 图对常量进行赋值的语句都将报错。
这里写图片描述

如果将某个成员方法修饰为final,则意味着该方法不能被子类覆盖,这就 和抽象方法必须由子类实现的规定互相矛盾,因此,final和abstract不能 同时修饰一个方法

这里写图片描述

public class test1{
    public final void foo(){
        System.out.println("final");
    }
}
class Cover extends test1{
    public final void foo(){
        System.out.println("Cover");
    } 
}

如果将某个类修饰为final,则说明该类无法被继承,一般语法

[访问权限] final class 类名 { 成员列表 }

Java中有一个最常用的final类:java.lang.String

static成员特征

static 在变量或方法之前,表明它们是属于类的,称为类方法(静态方法) 或类变量(静态变量)。若无static修饰,则是实例方法和实例变量 。

和类的其他成员属性不同,static成员并不存放在对象对应的堆空间中, 通过对JVM的分析发现,其会将static成员存放在方法区中,每个对象的 相应static共享同一段内存。

在成员变量前加static关键字,可以将其声明为静态成员变量 。

如果类中成员变量被定义为静态,那么不论有多少个对象,静态成员变量 只有一份内存拷贝,即所有对象共享该成员变量 。

静态成员变量的作用域只在类内部,但其生命周期却贯穿整个程序 。

在没有实例化对象时,可以通过类名访问静态成员变量。

也可以通过对象访问静态成员变量,但不论使用的是哪个对象,访问到的 都是同一个变量(因此建议不管是否有对象均由类名调用,更符合逻辑)。

这里写图片描述

public class test1{
    public static void main(String[] args) {
        A a = null;
        for(int i = 0; i < 5; i++){
            a = new A();
            new B();
        }
        System.out.println(a.number);
        System.out.println(B.number);//直接使用类名 调用静态成员

    }
}
class A{
    public int number = 0;//普通实例成员,构造方法每次只能对本对象 的counter自增,因此构建对象后,所有对 象的number值均为1

    public A(){
        number++;
    }
}
class B{
    public static int number = 0;//静态成员,所有对象共享,因此每构建一个 对象后该计数器累加

    public B(){
        number++;
    }
}

static final常量

可以将static和final联合起来使用。

当一个变量被static和final共同修饰时,其和仅用static修饰的变量的差异 在于声明时必须初始化,而且在整个程序生命周期内不能再对这个变量进 行二次赋值 。

static final常量通常用于保存整个应用程序共享的常量值。

静态成员方法没有this引用 。

和静态成员变量一样,可以通过类名或对象访问静态成员方法(建议使用 类名)。

static方法只能操作方法自身的局部 变量或类的static成员。

static方法和实例方法之间互相调用的情况

实例方法中可以直接调用static方法。

static方法中无法直接调用本类中声明的其他实例方法,如果需要调用, 只能在方法体中构建本类的对象,然后利用该对象调用实例方法 。

static方法可以直接调用本类中声明的其他static方法 。

实例方法中可以直接访问static成员变量而static方法中不能直接访问非 static的成员变量。

static import

这里写图片描述

简化后

这里写图片描述

static import也可以使用通配符,如import static java.lang.Math.*表示导入 Math类中所有的静态成员 。

使用import static语句,可以导入一个类里的一切被static修饰的东西,包括变 量、常量、方法和内类 。
去掉静态成员前面的类型名,固然有助于在频繁调用时显得简洁,但是同时也 失去了关于“这个东西在哪里定义”的提示信息,增加了阅读理解的麻烦。如果 导入的来源很著名(比如java.lang.Math),或者来源的总数比较少,这个问 题并不严重;但是在不属于这两种的情况下,这就不是基本可以忽略的问题了。

Static Import不能突破Java语言中原有的访问控制机制的限制,不过也并不在这方面增加新的 约束。原来有权限访问的静态成员,都可以被导入和使用;而原来无权限访问的静态成员, 用了这个方法之后也仍然是不能访问 。

同的类(接口)可以包括名称相同的静态成员。因此,在进行Static Import的时候,可能会 出现“两个语句导入同名的静态成员”的情况。在这种时候,Java会这样来加以处理:

如果两个语句都是精确导入的形式,或者都是按需导入的形式,那么会造成编译错误。

如果一个语句采用精确导入的形式,一个采用通配符*按需导入的形式,那么采用精确导入的形式的一 个有效。

this为什么不能在static方法中使用

之所以在非静态方法中可以使用this,是因为非静态方法参数传递时,有一个隐式参数this, 这个this就是调用该方法的对象本身比如。

static方法由于不和任何对象绑定,因此不能访问this

接口的作用与定义

[访问权限] interface 接口名 { 
    公开静态常量列表; 
    公开抽象方法列表;
} 

在Java中接口是一个比抽象类更加抽象的概念,由于只声明行为,因此在 接口中的方法均是抽象的。
这里写图片描述

  • 成员变量方面,在接口中只存在公开静态常量(即便没有使用static final修饰, Java也会默认确定其性质)

  • 成员方法方面,在接口中只存在公开抽象方法(即便没有abstract修饰,Java也会 默认其抽象性质)

这里写图片描述

类实现接口

[修饰符] class <类名> [extends 父类名] [implements 接口列表]{

} 

实现接口需要实现接口中声明的 所有方法,否则子类也需要声明 为抽象类.

接口的继承

新的接口可以继承自原有的接口,新接口将拥有原有接口的所有抽象方法 .

在声明接口之间的继承关系时,extends关键字后面可以是一个列表。

Interface3 extends Interface0, Interface1, interface……{ 
}

Java类的继承是单一继承,Java接口的继承是多重继承

不允许类多重继承的主要原因是,如果A同时继承B和C,而B和C同时有一个D方法,A无 法确定该继承那一个(钻石问题或者叫“讨嫌的菱形派生”(Dreadful Diamond onDerivation、DDD))

接口全都是抽象方法,不存在实现冲突,继承谁都可以,所以接口可继承多个接口。

JDK8接口的默认方法

默认方法是库/框架设计者的后悔药。

默认方法的声明很简单,问题是他带来的多继承问题如何解决?以下代码 不能编译,Clazz类 从A和B给foo()方法继承冲突的默认值。

这里写图片描述
为了修复上例的问题,在Clazz里我们必须手动通过重写冲突的方法解决, 或者显式调用某个接口中的方法。

这里写图片描述
这里写图片描述

解决多继承冲突的原则

情况一,接口IA有子接口IB1、IB2,而类C implements IB1,IB2:

如果仅仅IA定义默认方法m(),执行IA的默认方法。

如果只有IB1、IB2其中一个override IA定义的m(),调用“最具体接口”默认方法。

如果IB1、IB2都override IA定义的m(),而C不提供自己的方法体,则编译错误!因为 “最 具体接口”默认方法有两个。此时,C若Override m(),可以消去编译错误 。

类C提供自己的方法体时,可以提供自己的代码,也可以指定调用C implements 的直接 父接口的默认方法 。

小结:多个接口提供默认方法,则“最具体接口”默认方法胜出,但是不得出现多个“最具体 接口”。

情况二:接口IA(或IB1)定义了默认方法m(),类A1相同的方法m(),类C 是它们的子类型:

如果类A1提供了实现,按照a simple rule: “the superclass always wins.”,父类的 方法 被调用。

如果类A1不提供实现,即A1中m()为抽象方法,仍然按照the superclass always wins.类C需要override m(),给出自己的实现。否则,要么 C声明为抽象类,要么 编译错误。

小结:父类有默认方法的等价物,则接口默认方法如同不存在。

这里写图片描述

interface IA{
    default void foo(){
        System.out.println("IA");
    }
}

class A1{//父类实现
    public void foo(){
        System.out.println("A1");
    }
}

class test1 extends A1 implements IA{
    public static void main(String[] args) {
        C test = new C();
        test.foo();
    }
}

这里写图片描述

interface IA{
    default void foo(){
        System.out.println("IA");
    }
}

abstract  class A1{//父类未实现
    abstract  void foo();
}

class test1 extends A1 implements IA{
}

将test1声明为抽象类。
这里写图片描述

默认方法理论上抹杀了Java接口与抽象类的本质区别-前者是行为契约的 集合,后者是接口与实现的结合体。当然,语法上两者的差别和以前一样。 这就需要我们来自觉维护两者的本质区别,把默认方法作为库、框架向前 兼容的手段

定义常规的静态常量后使用存在的一些小问题

代码可读性差、易用性低。

类型不安全。

耦合性高,扩展性差。

枚举的声明

[访问权限] enum 枚举名{ 
枚举值列表 
} 

不能有public的构造函数,这样做可以保证客户代码没有办法新建一个enum的实例。

所有枚举值都是public , static , final的。注意这一点只是针对于枚举值,我们可以和在普通类 里面定义 变量一样定义其它任何类型的非枚举变量,这些变量可以用任何你想用的修饰符。

Enum默认实现了java.lang.Comparable接口。

Enum覆载了了toString方法 。

Enum提供了一个valueOf方法,这个方法和toString方法是相对应的 。

Enum还提供了values方法,这个方法使你能够方便的遍历所有的枚举值 。

Enum还有一个oridinal的方法,这个方法返回枚举值在枚举类种的顺序,这个顺序根据枚举 值声明的顺序而定。

public enum TypeEnum{
    A,B,C,D
}

switch语句enum类型,使用枚举,能让我们的代码可读性更强。

如果打算自定义自己的方法,那么必须在enum实例序列的最后添加一个 分号。而且 Java 要求必须先定义 enum 实例:

enum TypeEnum{
    A("a",1),B("b",2),C("c",3),D("d",4);
    private String name;
    private int index;
    private TypeEnum(String name,int index){
        this.name = name;
        this.index = index;
    }
    public static void traverse(){
        for(TypeEnum value : TypeEnum.values()){
            System.out.println(value.name);
        }
    }
}

public class test1{
    public static void main(String[] args) {
        TypeEnum.traverse();
    }
}

结果

a
b
c
d

所有的枚举都继承自java.lang.Enum类。由于Java 不支持多继承,所以枚 举对象不能再继承其他类

枚举可以实现接口。

几种关系的强弱如下:依赖 < 关联 < 聚合 < 组合

内部类的作用

内部类是Java独有的一种语法结构,即在一个类的内部定义另一个类,此 时,内部类I就成为外部类中的成员,访问权限遵循类成员的访问权限机 制,可以是public、protected、缺省和private。

内部类可以很方便地访问外部类中的其它成员。

根据内部类的定义,我们发现它能帮我实现一些特殊的需要:

完善多重继承
每个内部类都能独立地继承一个(接口的) 实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响。

形成闭包
内部类是面向对象的闭包,因为它不仅包含创建内部类的作用域的信息, 还自动拥有一个指向此外围类对象的引用,在此作用域内,内部类有权操 作所有的成员,包括private成员。

当我们在创建一个内部类的时候,它无形中就与外围类有了一种联系,依 赖于这种联系,它可以无限制地访问外围类的元素

内部类是个编译时的概念,一旦编译成功后,它就与外围类属于两个完全 不同的类(当然他们之间还是有联系的) 。

对于一个名为OuterClass的外围类和一个名为InnerClass的内部类,在编 译成功后,会出现这样两个class文件:OuterClass.class和 OuterClass$InnerClass.class 。

在Java中内部类主要分为成员内部类、局部内部类、匿名内部类、静态内 部类。

这里写图片描述

在成员内部类中要注意两点:

第一:成员内部类中不能存在任何static的变量和方法 。

第二:成员内部类是依附于外围类的,所以只有先创建了外围类才能够创建内部 类。

还有一种内部类,它嵌套在方法和作用域内的,对于这个类的使用主要是 应用与解决比较复杂的问题,想创建一个类来辅助我们的解决方案,又不 希望这个类是公共可用的,所以就产生了局部内部类,局部内部类和成员 内部类一样被编译,只是它的作用域发生了改变,它只能在该方法和作用 域中被使用,出了该方法和作用域就会失效。

这里写图片描述

局部内部类可以直接操作外部类的成员变量,但是对于方法的临时变量 (包括方法的参数,要求是final常量才能操作)

关键字static可以修饰成员变量、方法、代码块,其实它还可以修饰内部 类,使用static修饰的内部类我们称之为静态内部类或嵌套内部类。静态 内部类与非静态内部类之间存在一个最大的区别,非静态内部类在编译完 成之后会隐含地保存着一个引用,该引用是指向创建它的外部类,但是静 态内部类却没有。

没有这个引用就意味着:

它的创建是不需要依赖于外部类

不能使用任何外部类的非static成员变量和方法

和成员内部类不同,static内部类能够声明static的成员

由于内部类本质上是一个独立的类,因此在内部类中直接使用this,其指 代的是内部类自身的引用,如果要想引用外部内的对象,则应该使用外部类类名.this的方式

匿名内部类

如果一类内部类仅需要构建一个单一的对象,那么这个类其实并不需要额外取 一个特有的名字,对于不存在名字的内部类,我们称为匿名内部类 • 匿名内部类必须继承一个父类或实现一个接口 • 匿名内部类的声明使用方法如下:

[访问权限] [修饰符]父类名/接口名 引用名 = new 父类名/接口名([父类构 造方法参数列表]){ 
匿名内部类成员; 
}

这里写图片描述
匿名内部类没有构造方法(匿名内部类没有显式类名).

Lambda表达式

(parameters) -> expression 
或 
(parameters) ->{ 
statements; 
}

Lambda表达式语法示例:

不需要参数,返回值为 5 :() -> 5

接收一个参数(数字类型),返回其2倍的值: x -> 2 * x

接受2个参数(数字),并返回他们的差值: (x, y) -> x – y

接收2个int型整数,返回他们的和: (int x, int y) -> x + y

接受一个 string 对象,并在控制台打印,不返回任何值(看起来像是返回void): (String s) -> System.out.print(s)

和很多资料中介绍的不同,JDK8提供的lambda表达式并不是一个“语法糖”,它并 不会被编译生成传统带$的匿名内部类

lambda是方法的实现

lambda是延迟执行的

Lambda表达式对返回的实例类型有比较严格的要求:

• 必须是接口
• 接口中只有一个需要实现的抽象方法,因此如果接口中有超过1个抽象方法需要实 现的情况并不适用于lambda表达式

每一个lambda表达式都对应一个接口类型。而“函数式接口”是指仅仅只 包含一个抽象方法的接口,每一个该类型的lambda表达式都会被匹配到 这个抽象方法。因为默认方法不算抽象方法,所以也可以给函数式接口添 加默认方法

@FunctionalInterface 注解,编译器如果发现你标注了这个注解的接口有 多于一个抽象方法的时候会报错的

Lambda作用域

在lambda表达式中访问外层作用域和老版本的匿名内部类中的方式很相 似

可以直接访问标记了final的外层局部变量,或者实例的成员变量以及静态 变量

和匿名内部类不同的是,局部变量可以不用声明为final,代码同样能够正 确执行,但变量必须不可被表达式的代码修改(即隐性的具有final的语义)

Lambda表达式中是无法访问到默认方法的。

有一些因素推动促进了操作系统支持多程序同时执行的发展

资源利用:

程序有时候需要等待外部的操作,比如输入和输出,并且在等待的时候不可能进行有价值 的工作。在等待的时候,让其他的程序运行会提高效率

公平:

多个用户或程序可能对系统资源有平等的优先级别。让他们通过更好的时间片方式来共享 计算机,这要比结束个程序后才开始下一个程序更可取

方便:

写一些程序,让它们各自执行一个单独任务并进行必要的相互协调,这要比编写一个程序 来执行所有的任务更容易,更让人满意

程序、进程、线程的基本概念和关系

程序:

是计算机指令的集合.程序是一组静态的指令集,不占用系统运行资源,不能被系统 调度,也不能作为独立运行的单位,它以文件的形式存储在磁盘上

进程:

是一个程序在其自身的地址空间中的一次执行活动。比如,打开一个记事本,就 是调用了一个进程。进程是资源申请、调度和独立运行的单位,因此,它使用系 统中的运行资源;而程序不能申请系统资源,一个程序可以对应多个进程,例如 著名的QQ多开

线程

线 程允许程序控制流的多重分支同时存在于一个进程:

线程共享进程范围内的资源,比如内存和文件句柄,但是每一个线程有其自己的程序计数器、栈和本地 变量

线程也为多处理器系统中并行地使用硬件提供了一个自然而然的分解;同一程序内的多个线程可以在多 CPU的情况下同时调度

线程有些时候被称为轻量进程,并且大多数现代操作系统把线程作为时序调度的基本单元, 而不是进程,在没有明确协调的情况下,线程相互间同时或异步地执行。因为线程共亨其所 属进程的内存地址空间,因此所有同一进程中的线程访问相同的变量,并从同一个堆中分配 对象,这相对于进程间通信机制来说实现了良好的数据共享。但是如果没有明确的同步来管 理共享数据,一个线程可能会修改其他线程正在使用的数据,产生意外的结果。
这里写图片描述

多线程优点

可以更好的实现并行

恰当地使用线程时,可以降低开发和维护的开销,并且能够提高复杂应用的性能。

CPU在线程之间开关时的开销远比进程要少得多。因开关线程都在同一地址空间 内,只需要修改线程控制表或队列,不涉及地址空间和其他工作。

创建和撤销线程的开销较之进程要少。

Java在多线程应用中的优势

在JVM规范里没有规定的线程实现模型,具体的JVM实现用1:1(内核线 程)、N:1(用户态线程)、M:N(混合)模型的任何一种都是允许的。 Java并不暴露出不同线程模型的区别,上层应用是感知不到差异的,只是 性能特性会不太一样

Java SE最常用的JVM是Oracle/Sun研发的HotSpot VM。在这个JVM的较 新版本所支持的除了Solaris之外的平台上,它都是使用1:1线程模型的,而 在Solaris上,HotSpot VM使用的是M:N和1:1两种模型,1:1是默认值。

语言级别支持N:1线程模型为能够为Java带来一个好处:能够在受限的单线程 系统中支持多线程的Java应用,但是也会带来一些隐患。

只要一个应用线程产生了一次系统调用,比如I/O中断,那同一进程内的其他线程都会停 止。

不能利用多核,无法发挥多线程处理器优势 。

用于Java ME CLDC的CLDC HotSpot Implementation(CLDC-HI)支持两种线程模型, 默认使用N:1线程模型,所有Java线程都映射到一个内核线程上,是典型的用户态线程模 型;它也可以使用一种特殊的混合模型,Java线程仍然全部映射到一个内核线程上,但当 Java线程要执行一个阻塞调用时,CLDC-HI会为该调用单独启动一个内核线程,并且调度 执行其它Java线程,等到那个阻塞调用完成之后再重新调度之前的Java线程继续执行。

Java线程的生命周期

新线程:

当利用new关键字创建线程对象实例后,它仅仅作为一个对象实例存在, JVM没有为其分配CPU时间片和其他线程运行资源

就绪状态:

在处于创建状态的线程中调用start方法将线程的状态转换为就绪状态。这时,线程已经得到除CPU时间之外的其它系统资源,只等JVM的线程调度器按 照线程的优先级对该线程进行调度,从而使该线程拥有能够获得CPU时间片的机会

运行状态:

就绪态的线程获得cpu就进入运行态

等待/阻塞:

线程运行过程中被剥夺资源或者,等待某些事件就进入等待/阻塞状态, suspend()方法被调用 , sleep()方法被调用,线程使用wait()来等待条件变量; 线程处于I/O等待等,调用suspend方法将线程的状态转换为挂起状态。这时,线程将释放占用的所有资源,但是并不释放锁,所以容易引发死锁,直 至应用程序调用resume方法恢复线程运行。等待事件结束或者得到足够的资源就进入就绪态

死亡状态:

当线程体运行结束或者调用线程对象的stop方法后线程将终止运行,由JVM收回线程占用的资源

这里写图片描述

Thread类

这里写图片描述

对于Java而言,每一个独立的线程都是java.lang.Thread类的一个对象, 而线程中独立于其他线程所执行的指令代码由Thread类的run()方法提供 。

因此,要想在Java中实现多线程协作运行,就需要创建多个独立的 Thread类对象 。

而线程与线程之间所执行的指令代码会存在差异,由此可见,Java中实现 独立执行不同指令代码的多线程引用程序最简单直接的方法就是:继承 Thread类重写run方法并构建其对象。

这里写图片描述
注意:
1. 需要注意run()方法的签名
2. run()方法中的代码和其他方法中的代码区别在于它可以和其他线程的run()方法代码并行执行 2.初学者经常会有的一个误区是将线程和循环混淆,线程不具备任何循环特性,一旦run()方法代码 执行结束,线程的生命周期即会自动结束,因此如果需要在线程中循环执行某些代码需要自行声明 循环控制。

Thread类提供了较多的构造方法重载版本,以下列出了常用的几个版本。

构造方法说明
Thread()创建一个新的线程
Thread(String name)创建一个指定名称的线程
Thread(Runnable target)利用Runnable对象创建一个线程,启动时将执行该对象的run方法
Thread(Runnable target, String name)利用Runnable对象创建一个线程,并指定该线程的名称

Runnable接口

直接继承Thread类实现线程的方法存在局限性:由于Java是典型的单亲 继承体系,因此一旦类继承Thread之后就不能再继承其他父类,对于一 些必须通过继承关系来传播的特性这种方式显然会造成困扰 。

Runnable中只有一个签名和Thread中一致的run()方法,满足函数式接口的要求, 可以使用Lambda表达式 。

Runnable接口的子类并不是线程类,我们只是通过这种形式向线程类提供run()方 法指令代码,最后还需借助Thread类和Runnable接口的依赖关系和Thread类的 Thread(Runnable runnable)构造方法构建线程对象。

实现Runnable接口构建线程:
这里写图片描述

public class test1 implements Runnable{

    public void run(){
        System.out.println("I'm run.");
    }
    /*Runnable接口中仅有一个run()方法,
    该方法签名与Thread类中的 run()方法签名一致*/
    public static void main(String[] args) {
        test1 test =new test1();
        Thread thread = new Thread(test);
    }
    /*切记Runnable不是线程类,而是一个实现线程为其提供run()方法指令代码的工具, 
    因此构建其对象之后需要将其作为Thread类的构造方法参数以构建实际的线程对象*/
}

Runnable+内部类
(lambda)
这里写图片描述

public class test1{
    public static void main(String[] args) {
        Runnable test = ()->System.out.println("内部类,Runnable。");
        Thread thread = new Thread(test);
    }
}

线程启动和停止

任何一个Java程序启动时,一个线程立刻运行,它执行main方法,这个 线程称为程序的主线程;也就是说,任何Java程序都至少有一个线程,即 主线程

主线程的特殊之处在于

它是产生其它线程子线程的线程;

通常它必须最后结束,因为它要执行其它子线程的关闭工作。

在主线程中启动其他线程,不能直接调用线程对象的run()方法(线程对 象的run()方法可以显式调用,但在代码中显式调用run()方法不能产生并 发调用的作用),而应该使用线程对象的start()方法 。
这里写图片描述

如果一个线程由于等待某些事件的发生而被阻塞,又该怎样停止该线程呢。

这种情况经常会发生,比如当一个线程由于需要等候资源、数据而调用 Thread.sleep()方法、Object类的wait()方法等,都有可能导致线程阻塞, 使线程处于处于不可运行状态时,即使主程序中将该线程的共享变量设置 为false,但该线程此时根本无法检查循环标志,当然也就无法立即中断 。

这里我们给出的建议是,使用Thread提供的interrupt()方法,因为该方法 虽然不会中断一个正在运行的线程,但是它可以使一个被阻塞的线程抛出 一个中断异常,从而使线程提前结束阻塞状态,退出堵塞代码。

interrupt()方法
这里写图片描述

在Thread类中有两个方法可以判断线程是否通过interrupt方法被终止:

一个是静态的方法interrupted()

一个是非静态的方法isInterrupted()

这两个方法的区别是interrupted用来判断当前线是否被中断,而isInterrupted可以用 来判断其他线程是否被中断 。

首先,需要区分捕获到的InterruptedException是否是由我们主动调用interrupt()方法所引起的,因 为还有其他因素可能会导致此异常,而在某些时候即便是产生了此异常也应该能够让线程继续执行, 这时候我们就需要编写代码来进行区分 。

另一个不太好的消息是, interrupt()方法并不能阻断I/O阻塞或线程同步引起的线程阻塞,也就是 说如果有一个线程在等待键盘输入或是等待网络连接等I/O资源,之前的停止方法就会失效。

解决办法:关闭底层I/O通道,人为引发异常从而进行共享变量重新赋值而跳 出线程的run()方法。

中断I/O等待线程示例:
这里写图片描述

线程不同状态之间的转换

这里写图片描述

守护线程 (Daemon Thread)

所谓守护线程,是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回 收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因 此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线 程。反过来说,只要任何非守护线程还在运行,程序就不会终止 。

用户线程和守护线程两者几乎没有区别,唯一的不同之处就在于虚拟机的退出:如果 用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。 因为没 有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了。

将线程转换为守护线程可以通过调用Thread对象的setDaemon(true)方法 来实现(默认守护线程的属性为false,即默认创建的线程对象为非守护 的用户线程),在使用守护线程时需要注意一下几点:

thread.setDaemon(true)必须在thread.start()之前设置,否则会抛出一个 IllegalThreadStateException异常,不能把正在运行的常规线程设置为守护线程

在Daemon线程中产生的新线程也是Daemon的 。‘

不是所有的应用都可以分配给Daemon线程来进行服务,比如读写操作或者计算 逻辑。因为在Daemon Thread还没来的及进行操作时,虚拟机可能已经退出了 • 可以通过线程对象的isDaemon()方法判定该线程是否守护线程。

每个线程可以存在一个给定的名字,线程名字除了Thread类的构造方法 可以提供以外,也可以通过setName(String name)方法给定 .

getName()方法可以获取特定线程的名字 。

如果没有为线程显式提供名字,Java将按照thread-0,thread-1…threadn的方式为线程提供默认的名字(由main方法启动的主线程名字为: main)。

Java线程可以有优先级的设定,高优先级的线程比低优先级的线程有更高的几率得到 执行(注意是更高的几率,而不是优先级高的一定有优势,但是优先级在某一些线程 调度方法中有特定的作用) 。

Java线程的优先级是一个整数,其取值范围是1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY ) 。

除了Thread.MIN_PRIORITY和Thread.MAX_PRIORITY外,Thread还提供了另一个常 量Thread.NORM_PRIORITY(5),但是需要注意的是,通过对Thread类源码的解析, 会发现事实上Java线程在没有明确指定的情况下,其优先级并不一定是 NORM_PRIORITY,而是和父线程(创建本线程的线程)的优先级保持一致,main线 程的优先级是NORM_PRIORITY。

系统线程组的最大优先级默认为Thread.MAX_PRIORITY • 创建线程组的时候其最大优先级默认为父线程组(如果未指定父线程组,则其父线程组默认为当前线程所属线程组)的最大优先级

可以通过setMaxPriority更改最大优先级,但无法超过父线程组的最大优先级 。

setMaxPriority的问题:

该方法只能更改本线程组及其子线程组(递归)的最大优先级。

但不能影响已经创建的直接或间接属于该线程组的线程的优先级,也就是说,即使目前有一个子线 程的优先级比新设定的线程组优先级大,也不会更改该子线程的优先级。只有当试图改变子线程的 优先级或者创建新的子线程的时候,线程组的最大优先级才起作用。

Thread.setPriority()可能根本不做任何事情。

线程的优先级通常是全局的和局部的优先级设定的组合。Java的setPriority()方法只应用于 局部的优先级。这通常是一种保护的方式,任何用户都不希望鼠标指针的线程或者处理音 频数据的线程被其它随机的用户线程所抢占 。

这里写图片描述

这里写图片描述

class Thread1 extends Thread{
    public void run(){
        for(int i = 1; i < 10; i++){
            System.out.println(">>>  Thread1 " + i);
        }
    }
}
class Thread2 extends Thread{
    public void run(){
        for(int i = 1; i < 10; i++){
            System.out.println("^^^  Thread2 " + i);
        }
    }

    public Thread2(ThreadGroup group,String name){
        super(group,name);
    }

}
public class test1 {
    public static void main(String[] args) {
        Thread thread1=new Thread1();
        ThreadGroup group =new ThreadGroup("Mygroup");
        Thread thread2=new Thread2(group,"t2");
        thread1.setPriority(6);
        thread1.setName("t1");
        System.out.println(thread1);
        System.out.println(thread2);
        thread1.start();
        thread2.start();
    }
}

线程可以通过调用特殊方法来放弃自己占据的部分资源,Thre
ad类中的 两个静态方法提供了这个功能:

sleep() 
yield()

sleep()

使当前线程进入阻塞状态,所以执行sleep()的线程在指定的时间内肯定不会执行, 同时sleep方法不会释放锁资源(即如果正在运行的线程占有某个资源的同步锁,它不会释放掉这个同步锁,其他线 程仍然不能访问该资源,详情参加下一节的线程同步)

sleep()可使优先级低的线程得到执行的机会,当然也可以让同优先级和高优先级的线程有执行的机会.

sleep()方法有两个版本:sleep(long millis)和sleep(long millis,int nanos),第一个版本需要提供以 毫秒为单位的阻塞时间,第二个版本在毫秒的基础上可以附加0-999999的纳秒值 。

Java并不保证线程在阻塞给定的时间后能够马上执行(事实上这几乎是不可能的事情),在阻塞时 间到了之后,线程进入就绪状态,继续执行的时机取决于Java虚拟机的线程调度机制,唯一能够确 定的是,线程中断执行的时间是大于等于给定的阻塞时长的,因此不要将sleep用作精确度要求非 常高的定时任务调度。

yield()

yield() 方法只是使当前线程重新回到就绪可执行状态,所以执行yield()线程有可能在进入 到就绪状态后马上又被执行,只能使相同或更高优先级的线程有执行的机会 。

同样, yield()也不会释放锁资源 。

sleep和yield的区别在于, sleep需要提供阻塞时长,可以使优先级低的线程得到 执行的机会, 而yield由于使线程直接进入就绪状态,没有阻塞时长,而且只能 使相同或更高优先级的线程有执行的机会,甚至于某些时候JVM认为不符合最 优资源调度的情况下会忽略该方法的调用(类似于System.gc())。

这里写图片描述

class ThreadTest extends Thread{
    public String type;
    public void run(){
        for(int i = 1; i < 10; i++){
            Thread.yield();
            System.out.println(type + i);
        }
    }
}
public class test1 {
    public static void main(String[] args) {
        ThreadTest one = new ThreadTest();
        ThreadTest two = new ThreadTest();
        one.type = ">>>";
        two.type = "^^^";
        one.start();
        two.start();
    }
}

这里写图片描述

class ThreadTest extends Thread{
    public String type;
    public void run(){
        for(int i = 1; i < 10; i++){
            try{
                Thread.sleep(100);
            }
            catch(InterruptedException ex){
                System.out.println("Interrupted!");
            }
            System.out.println(type + i);
        }
    }
}

主函数同上

join()

Thread类的join()方法用于等待其它线程结束,当前运行的线程可以调用另一线 程的join方法,当前运行线程将转到阻塞状态,直至另一线程执行结束,它才 会恢复运行,也就是说,t.join()方法阻塞调用此方法的线程(calling thread), 直到线程t完成,此线程再继续 。

通常用于在main()主线程内,等待其它线程完成再结束main()主线程 。

通过对源代码的解析发现join方法实现是通过wait。 当main线程调用t.join时候, main线程会获得线程对象t的锁,调用该对象的wait(),直到该对象唤醒main线 程 ,比如退出后。这就意味着main 线程调用t.join时,必须能够拿到线程t对象 的锁(下一节详细介绍同步锁)。

不同步会发生的问题

Java为了保证其平台无关性,使应用程序与操作系统内存模型隔离开,定 义了自己的内存模型 。

在Java内存模型中,内存分为主内存和工作内存两个部分,其中主内存是 所有线程所共享的,而工作内存则是每个线程分配一份,各线程的工作内 存间彼此独立、互不可见,在线程启动的时候,虚拟机为每个内存分配一 块工作内存,不仅包含了线程内部定义的局部变量,也包含了线程所需要 使用的共享变量(非线程内构造的对象)的副本,即为了提高执行效率,在 副本中寻址并读取数据比直接读取主内存更快。

Java内存模型定义了一系列工作内存和主内存之间交互的操作及操作之间 的顺序的规则,对于共享普通变量来说,约定了变量在工作内存中发生变 化了之后,必须要回写到主内存,但是这个约定只规定了结果而并没有规 定时机,因此有可能在实际写回主内存之前,有很多其他线程已经在朱内 存中读取到了已经无效的数据。

让刚刚从单线程阵营转变到并发开发的同学更难以发觉处理的问题是JVM的指令重排序 。

重排序通常是编译器或运行时环境为了优化程序性能而采取的对指令进行重新排序执 行的一种手段 。

重排序通过调整指令顺序,在不改变程序语义的前提下,尽可能减少寄存器的 读取、存储次数,充分复用寄存器的存储值 。

因此,对于以下两句代码:

 int a = 10; int b = 20; 

在计算机执行的时候,有可能第二条语句会先于第一条语句执行。所以, 千万不要随意假设指令执行的顺序。

在单线程环境下,指令执行的最终效果 应当与其在顺序执行下的效果一致,否则这种优化便会失去意义。

当代码中存在控制依赖性时,会影响指 令序列执行的并行度。

为此,编译器和处理器会采用猜测(Speculation)执行来克服 控制相关性对并行度的影响。以处理器的猜测执行为例,执行线程B的处理器可以提 前读取并计算a*a,然后把计算结果临时保存到一个名为重排序缓冲(reorder buffer ROB)的硬件缓存中。当接下来操作3的条件判断为真时,就把该计算结果写入变量i 中。

猜测执行实质上对操作3和4做了重排序。重排序在这里也破坏了多线程程序的语义

在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果(这也是as-ifserial语义允许对存在控制依赖的操作做重排序的原因);但在多线程程序中,对存在 控制依赖的操作重排序,可能会改变程序的执行结果。

synchronized关键字的使用

volatile,它用于在声明时修饰变量。

保证变量在内存模型中的可见性 。
禁止指令重排序:对volatile变量的操作不能进行重排序。

保证内存可见性

对于volatile变量Java要求工作内存中发生变化之后,必须马上回写到主内存,而 线程读取volatile变量的时候,必须马上到主内存中去取最新值而不是读取本地工 作内存的副本 。

此规则保证了 “当线程A对变量X进行了修改后,在线程A后面执行的其他线程能看 到变量X的变动”。

使用volatile修饰变量不能保证对其操作的原子性。

类似于数据库学到的“丢失修改”。

在现代的Java虚拟机中,只有在对变量读取频率很高的情况下,虚拟机才 不会及时回写主内存,而当频率没有达到虚拟机认为的高频率时,普通变 量和volatile具备同样的处理逻辑 。

原子操作

当一个线程在没有特殊保护的情况下读取变量,它可能会得到一个过期值。但是至少它可以 看到某个线程在那里设定的一个真实数值,而不是一个凭空而来的值,这样的安全保证被称 为是最低限的安全性。最低限的安全性应用于所有的变量,除了一个例外:没有声明为 volatile的64位数值变量(double和long) 。

Java存储模型要求获取和存储操作都为原子的,但是对于非volatile的long和double变量JVM 允许将位的读或写划分为两个32位的操作,如果读和写发生在不同的线程,这种情况读取一 个非volatile类型long就可能会出现得到一个值的高32位和另一个值的低32位,因此,即使你 并不关心过期数据,但仅仅在多线程程序中使用共享的、可变的long和double变量也可能是 不安全的。除非将它们声明为volatile类型,或者用锁保护起来(由此可见,Java对32位以内 数据的load和save操作都是原子的)。

如果需要将多条代码视作一个整体调度单元,希望这个调度单元在多线程 环境中的调度顺序不影响任何结果,我们需要对代码提供比volatile更严 格的保护,除了保证可见性、防止重排序改变语义之外,还要将该代码段 进行原子保护,这种保护我们称为线程同步,其主要的作用是实现线程安 全的类

synchronized关键字的使用

在JVM中,每个对象和类在逻辑上都是和一个监视器相关联的,为了实 现监视器的排他性监视能力(即保证资源只能同时被一个线程访问), JVM为每一个对象和类都关联一个,锁住了一个对象,就是获得对象相 关联的监视器 。

临界区同步块

有时,你只是希望防止多个线程同时访问方法内部的部分代码而不是整个方法

通过这种方式分离出来的代码被称为 “临界区” (critical section),它也使用 synchronized关键字建立。

这里, synchronized被用来指定某个对象,此对象的锁被用来对花括号内的代码进行 同步控制 。

临界区同步块可以适当降低同步整个方法带来的性能消耗。

线程死锁

互斥条件:线程使用的资源中至少有一个是不能共享

至少有一个进程持有一个资源,并且它在等待获取一个当前被别的进程持有的资源.

资源不能别进程抢占.所有的进程必须把资源释放作为普通事件.

必须有循环等待,这时,一个进程等待其它进程持有的资源,后者又在等待另一个进程持有的 资源,这样一直下去,直到有一个进程在等待第一个进程持有的资源,使得大家都被锁住.

小结

volatile能够保证可见性,能够 保证语义顺序,但是不能保证原子性 。

long/double这两个64位的数据写入不能保证原子性 。

使用关键字synchronized实现线程同步,可以直接修饰方法或者修饰代码 块,在修饰代码块时需要提供一个对象作为监视器锁。

wait/notify/notifyAll方法的使用

忙等待没有对运行等待线程的CPU进行有效的利用,除非平均等待时间非 常短。否则,让等待线程进入睡眠或者非运行状态更为明智,直到它接收 到它等待的信号。

一个线程一旦调用了任意对象的wait()方法,就会变为非运行状态,直到另一个线程 调用了同一个对象的notify()/notifyAll()方法 。

为了调用wait()或者notify(),线程必须先获得那个对象的锁。也就是说,线程必须在 同步块里调用wait()或者notify(),JVM是这么实现的,当你调用wait时候它首先要检查 下当前线程是否是锁的拥有者,不是则抛出IllegalMonitorStateException。
此处多注意。

当一个线程调用一个对象的notify()方法,正在等待该对象的所有线程中将有一个线程 被唤醒并允许执行(这个将被唤醒的线程是随机的,不可以指定唤醒哪个线程)。同 时也提供了一个notifyAll()方法来唤醒正在等待一个给定对象的所有线程 。

一旦线程调用了wait()方法,它就释放了所持有的监视器对象上的锁。这将允许其他 线程也可以调用wait()或者notify()(这是wait方法和Thread类sleep方法的主要区别)。

wait()方法也具备有时间参数的重载版本,超时时间到了之后即使没有其他线程调用 notify方法,线程也将唤醒(如果超时时间设置为0,则和无参版本wait方法功能保持 一致)

notify()和notifyAll()方法不会保存调用它们的方法,因为当这两个方法被 调用时,有可能没有线程处于等待状态。通知信号过后便丢弃了。因此, 如果一个线程先于被通知线程调用wait()前调用了notify(),等待的线程将 错过这个信号 。

在某些情况下,这可能使等待线程永 远在等待,不再醒来,因为线程错过了唤醒信号。

在空字符串作为锁的同步块(或者其他常量字符串)里调用wait()和notify()会产生一些问题:

JVM/编译器内部会把常量字符串转换成同一个对象。这意味着,即使你有2个不同的实例, 它们都引用了相同的空字符串实例。同时也意味着存在这样的风险:在第一个实例上调用 wait()的线程会被在第二个实例上调用notify()的线程唤醒:

这可能不像个大问题。毕竟,如果notify()在第二个实例上被调用,真正发生的事不外乎线程一被错误的 唤醒了,这个被唤醒的线程将在while循环里检查信号值,然后回到等待状态,因为改变共享变量状态的 操作并没有在第一个实例上调用,而这个正是它要等待的实例。

这种情况相当于引发了一次假唤醒。线程在信号值没有更新的情况下唤醒。但是代码处理了这种情况, 所以线程回到了等待状态。但是真正应该唤醒的第二个线程却丢失了唤醒信号

你可能会设法使用notifyAll()来代替notify(),但是这在性能上是个坏主意。在只有一个线程能对信号进 行响应的情况下,没有理由每次都去唤醒所有线程

在wait()/notify()机制中,不要使用全局对象,字符串常量等。应该使用对应唯一的对象

管道流通讯

Java在它的jdk文档中提到不要在一个线程中同时使用管道输入流和管道 输出流,这可能会造成死锁。

线程池概念与作用

线程池主要用来解决线程生命周期开销问题和资源不足问题。通过对多个 任务重复使用线程,线程创建的开销就被分摊到了多个任务上了,而且由 于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。这样, 就可以立即为请求服务,使用应用程序响应更快。

通过适当的调整线程中的线程数目可以防止出现资源不足的情况 。

当一个服务器接受到大量短小线程的请求时,使用线程池技术是非常合适 的,它可以大大减少线程的创建和销毁次数,提高服务器的工作效率。

一个比较简单的线程池至少应包含:

• 线程池管理器
创建、销毁并管理线程池,将工作线程放入线程池中
• 工作线程
一个可以循环执行任务的线程,在没有任务是进行等待
• 任务列队
提供一种缓冲机制,将没有处理的任务放在任务列队中
• 任务接口
每个任务必须实现的接口,主要用来规定任务的入口、任务执行完后的收尾工作、任务的执行 状态等,工作线程通过该接口调度任务的执行

信号量的概念与作用

信号量,有时被称为信号灯,是在多线程环境下使用的一种设施, 它负责 协调各个线程, 以保证它们能够正确、合理的使用公共资源。

一个计数信号量。从概念上讲,信号量维护了一个许可集。如有必要,在 许可可用前会阻塞每一个资源申请请求,然后再获取该许可。信号量提供 一个方法添加一个许可,从而可能释放一个正在阻塞的获取者 。

信号量对可用许可进行计数,并采取相应的行动。拿到信号量许可的线程 可以进入代码,否则就等待。通过申请和释放方法获取和释放访问许可。

生产者-消费者模式

生产者仅仅在仓储未满时候生产,仓满则停止生产

消费者仅仅在仓储有产品时候才能消费,仓空则等待

当消费者发现仓储没产品可消费时候会通知生产者生产

生产者在生产出可消费产品时候,应该通知等待的消费者去消费

线程池调度器

引入的Executor框架的最大优点是把任务的提交和执行解耦,要执行任务的人只需把Task描述清楚,然 后提交即可。这个Task是怎么被执行的,被谁执行的,什么时候执行的,提交的人就不用关心了。具 体点讲,提交一个Callable对象给ExecutorService(如最常用的线程池ThreadPoolExecutor),将得到 一个Future对象,调用Future对象的get方法等待执行结果就好了 .

Executor框架的重要核心接口和类:

Executor是一个可以提交可执行任务的工具,这个接口解耦了任务提交和 执行细节(线程使用、调度等),Executor主要用来替代显示的创建和运 行线程

ExecutorService提供了异步的管理一个或多个线程终止、执行过程 (Future)的方法

Executors类提供了一系列工厂方法用于创建任务执行器,返回的任务执 行器都实现了ExecutorService接口(绝大部分执行器完成了池化操作)

execute(Runnable)

接收一个java.lang.Runnable对象作为参数,并且以异步的方式执行它,使用这种 方式没有办法获取执行Runnable之后的结果,如果你希望获取运行之后的返回值, 就必须使用 接收Callable参数的execute() 方法 .

submit(Runnable)

同样接收一个Runnable的实现作为参数,但是会返回一个Future 对象。这个 Future对象可以用于判断Runnable任务是否结束执行

submit(Callable)

和方法submit(Runnable)比较类似,但是区别在于它们接收不同的参数类型。Callable的 实例与Runnable的实例很类似,但是Callable的call()方法可以返回一个结果而方法 Runnable.run()则不能返回结果
Callable的返回值可以从方法submit(Callable)返回的Future对象中获取

inVokeAny()

接收一个包含Callable对象的集合作为参数。调用该方法不会返回Future对象,而是返回 集合中某个Callable对象的结果,而且无法保证调用之后返回的结果是集合中的哪个 Callable结果,只知道它是这些Callable中的一个 。

如果一个任务运行完毕或者抛出异常,方法会取消其它的Callable的执行。

invokeAll()

会调用存在于参数集合中的所有 Callable 对象,并且返回一个包含Future对象的 集合,可以通过这个返回的集合来管理每个Callable的执行结果 。

需要注意的是,任务有可能因为异常而导致运行结束,所以它可能并不是真的成 功运行了。但是我们没有办法通过 Future 对象来了解到这个差异。

当使用 ExecutorService 完毕之后,我们应该关闭它,这样才能保证线程不会继续保持运行状态 。

举例来说,如果你的程序通过main() 方法启动,并且主线程退出了你的程序,如果你还有一个活动的 ExecutorService存在于程序中,那么程序将会继续保持运行状态。存在于ExecutorService中的活动线 程会阻止Java虚拟机关闭 。

为了关闭在 ExecutorService 中的线程,需要调用 shutdown()方法。ExecutorService 并不会马上关闭, 而是不再接收新的任务,一旦所有的线程结束执行当前任务,ExecutorServie才会真的关闭。所有在调 用shutdown()方法之前提交到ExecutorService的任务都会执行。

如果希望立即关闭ExecutorService,可以调用shutdownNow()方法。它会尝试马上关闭所有正在执行 的任务,并且跳过所有已经提交但是还没有运行的任务。但是对于正在执行的任务,是否能够成功关 闭它是无法保证的,有可能他们真的被关闭掉了,也有可能它会壹直执行到任务结束.

Runnable与Callable

Runnable与Callable两个接口的区别在于:

Runnable提供的任务方法为run,它不能返回任何的结果

Callable接口提供的任务方法为call,它可以返回任务执行后的结果

两个接口都符合函数式接口的标准

锁对象

JDK5中有一个Lock的默认实现:ReentrantLock

可重入的独占锁。该对象与synchronized关键字有着相同的表现和更清晰的语义,而且还具有一些 扩展的功能。可重入锁被最近的一个成功lock的线程占有(unlock后释放)。该类有一个重要特性 体现在构造器上,构造器接受一个可选参数,是否是公平锁,默认是非公平锁

公平锁:

先来一定先排队,一定先获取锁

非公平锁:

不保证上述条件。非公平锁的吞吐量更高

这里写图片描述

ThreadLocal

线程内变量共享工具

ThreadLocal并不是一个Thread,而是Thread的局部变量,当使用ThreadLocal维护变 量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每个线程都 可以独立地改变自己的副本,而不会影响其它线程所对应的副本 .

从线程的角度看,目标变量就象是线程的本地变量,这也是类名中“Local”所要表 达的意思.

void set(T value)

将此线程局部变量的当前线程副本中的值设置为指定值

void remove()

移除此线程局部变量当前线程的值

protected T initialValue()

返回此线程局部变量的当前线程的“初始值”

T get()

返回此线程局部变量的当前线程副本中的值

原子操作类

Atomic[数据类型]

Timer

Timer是Java最早提供的一个任务调度器,它可以支持定时任务和重复任 务的调度。

Timer通过一个独立的线程通过wait/notify机制对所有的任务进行调度, 但是用户无需关心内部的线程实现细节,仅需通过Timer类的相关调度方 法即可实现任务的调度。

这里写图片描述

Quartz任务调度

Quartz框架的核心是调度器。调度器负责管理Quartz应用运行时环境。 调度器不是靠自己做所有的工作,而是依赖框架内一些非常重要的部件。 Quartz不仅仅是线程和线程管理。为确保可伸缩性,Quartz采用了基于 多线程的架构。启动时,框架初始化一套worker线程,这套线程被调度 器用来执行预定的作业。这就是Quartz怎样能并发运行多个作业的原理。 Quartz依赖一套松耦合的线程池管理部件来管理线程环境。

小结

Timer是Java最早提供的一个任务调度器,它可以支持定时任务和重复任务的调 度,如果希望借助于Timer进行任务调度,必须要满足Timer对任务本身的抽象 规定,从刚才的Timer调度方法列表中可以看出,所有的方法第一个共同的参 数均为TimerTask的对象,用于声明需要调度的任务中需要执行的指令代码 • 一个Timer调度器的所有任务都运行在一个线程中,存在单个任务出现异常导 致所有任务不能执行的隐患,而JDK5之后的ScheduledThreadPoolExecutor提 供了并发任务调用,不存在这个隐患 • Quartz是一个完全由java编写的开源任务调度框架

File类型

File类的构造方法有4种重载方式,常用的如下:

File(String pathname)

指定文件(或目录)名和路径创建文件对象

File f1 = new File(“chinasofti.txt"); 
/**针对当前项目根目录中的chinasofti.txt文件构 建了一个File对象
/
File f2 = new File("D:\\Java\\Hello.java");
/*通过绝对路径构建File对象*/

提供给构造方法的路径可以指向一个具体的文件,这时候File对象能够操作这个文件的属性,也可以指 向一个文件夹,这时候File对象操作的就是文件夹的属性 .

特别注意,Java中的相对路径体系和我们日常所见的文件系统相对路径体系有较大的区别:

如果以路径以“/”或“\”开头,则相对路径的根为当前项目所在磁盘的根目录(Unix没有磁盘分区的概念,因此直接使 用/,即文件系统的根作为相对路劲的根)

如果不以“/”开头则相对路径的根为项目根目录,而不是当前类所在目录,这一点非常容易引起误区,因为类从属于 某个包之后,类文件实际是位于项目中的某个子文件夹中的,如com.chinasoft.Hello这个类是位于项目中的 com\chinasofti子文件夹中,如果在Hello类中构建一个File对象:FIle f = new File(“icss/chinasofti.txt”),那么这个文 件位于项目根目录的icss子文件中,跟当前类自己的位置无关

这里写图片描述

这里写图片描述

输入输出流

当数据在逻辑上的总线上传输时,发生总线位宽大小变 化,都将需要一个缓存和流式串行传输的过程,因此我们把操作这个过程 的工具称为:流(Stream)

逻辑总线由宽变窄的过程是高位宽并发传输->缓存->低位宽流式串行传输

逻辑总线由窄变宽的过程是低位宽流式串行传输->缓存->高位宽并发传输。

不管如何变化,但是只要发生了逻辑总线位宽变化时均需要使用流。

流的特点:

流是一串连续不断的数据的集合,就象水管里的水流,在水管的一端一点一点地 供水,而在水管的另一端看到的是一股连续不断的水流。数据写入程序可以是一 段、一段地向数据流管道中写入数据,这些数据段会按先后顺序形成一个长的数 据流。对数据读取程序来说,看不到数据流在写入时的分段情况,每次可以读取 其中的任意长度的数据,但只能先读取前面的数据后,再读取后面的数据。不管 写入时是将数据分多次写入,还是作为一个整体一次写入,读取时的效果都是完 全一样的 。

流是磁盘或其它外围设备中存储的数据的源点或终点。

Java中输入输出流的类型

字节流和字符流

字节流是指8位的通用字节流,以字节为基本单位,在java.io包中,对于字节流进 行操作的类大部分继承于InputStream(输入字节流)类和OutputStream(输出 字节流)类。

字符流是指16位的Unicode字符流,以字符(两个字节)为基本单位,非常适合处 理字符串和文本,对于字符流进行操作的类大部分继承于Reader(读取流)类和 Writer(写入流)类。

Java I/O主要包括如下几个层次,包含三个部分:

流式部分:

IO的主体部分;

非流式部分:

主要包含一些辅助流式部分的类,如:File类、RandomAccessFile类 和FileDescriptor等类;

其他类:

文件读取部分的与安全相关的类,如:SerializablePermission类,以及与 本地操作系统相关的文件系统的类,如:FileSystem类和Win32FileSystem类和 WinNTFileSystem类。
这里写图片描述

传统输入流的读取线程特性

java.io.*包是Java中最传统的IO操作(相对于nio),在利用io包的输入流工具(InputStream、Reader)进行数据读取 操作时,如果无法读取到需要的内容,将会导致执行读取数据操作的线程 陷入阻塞状态

因此,在进行流的写入和读取时要尤为小心,保证数据读取的数量和顺序 的准确性,否则可能导致应用执行异常。

Java中如果需要将数据缓存在内存中,那么使用哪种类型最为合适?

如果需要将Java中能够表达所有数据缓存在内存中(包括字符类型或二进 制类型),最适宜于使用的数据类型是:byte[]

原因是Java中所有的数据类型占据的空间都是byte型的整数倍数。

字节输出流

OutputStream是一个抽象类,提供了Java向流中以字节为单位写入数据 的公开接口,大部分字节输出流都继承自InputStream类,重要的写入方 法如下:

void write(byte[] b, int off, int len)
//从off开始的len个字节数据将被写到流中

这里写图片描述

先 flush() 然后close()。

DataOutput

DataOutput接口规定一组操作,用于以一种与机器无关(当前操作系统 等)的方式,直接向流中写入基本类型的数据和字符串:

DataInput对基本数据类型的写入分别提供了不同的方法,方法名满足**writeXXX()**的规律,其中XXX即为基本类型说明符(首字母大写),如writeInt()表示向流中写 入一个int型数据

写入字符串的方法为**writeUTF()**,该方法的功能声明将标准的UTF-8字符编码表示 形式做出了稍许修改.

常见字节输出流工具的作用与使用

FileOutputStream类,它称为文件输出流,继承于OutputStream类, 是进行文件内容写操作的最基本类工具 。

FilterOutputStream是用来封装其它的输出流,并为它们提供额外的功能。它 的实现主要包括BufferedOutputStream, DataOutputStream和PrintStream 。

BufferedOutputStream的作用就是为输出流提供缓冲功能 。

DataOutputStream 是用来装饰其它输出流,它实现了DataOutput接口,将 DataOutputStream和DataInputStream输入流配合使用,允许应用程序以与机器无关方式 从底层输入流中读写基本 Java 数据类型和字符串 。

PrintStream 是用来装饰其它输出流。它能为其他输出流添加了功能,使它们能够方便地 打印各种数据值表示形式。

FileOutputStream能够将内存中的数据输出到文件中。

这里写图片描述
FileOutputStream和DataOutputStream结合使用示例

import java.io.*;
public class test1 {
    public static void main(String[] args) {
        FileOutputStream fos = null;
        DataOutputStream dos = null;
        try{
            File file = new File("data.txt");
            fos = new FileOutputStream(file,true);
            dos = new DataOutputStream(fos);
            dos.writeInt(1998);
            dos.writeDouble(325.00);
            dos.writeUTF("I/O test.");
        }catch(Exception ex){
            ex.printStackTrace();
        }finally{
            if(dos != null)
                try{
                    dos.close();
                }catch(IOException ex){
                    ex.printStackTrace();
                }
            if(fos != null)
                try{
                    fos.close();
                }catch(IOException ex){
                    ex.printStackTrace();
                }
        }
    }
}

字节输入流的统一数据读取方法

InputStream提供了针对数据流读取的公共接口,其中比较重要的数据读 取方法如下:
int read(byte[] b, int off, int len) 

返回值表示 本次读取实 际读取到的 字节个数, 如果已经到 流的末尾, 不能读取到 任何数据, 则返回-1。

可以用于存放本次读取数据的长度: len

实际读取的数据,个数即为方法的返回值。

用于数据读取 操作的byte[]。

off为起始点。

这里写图片描述

DataInput

DataInput接口规定一组操作,用于以一种与机器无关(当前操作系统等)的方 式,直接在流中读取基本类型的数据和字符串:
DataInput对基本数据类型的读取分别提供了不同的方法,方法名满足readXXX()的规律, 其中XXX即为基本类型说明符(首字母大写),如readInt()表示从流中读取一个int型数据
读取字符串的方法为readUTF(),该方法的功能声明将标准的UTF-8字符编码表示形式做 出了稍许修改,接口还提供了readLine()方法,但是在一些常用的实现中不建议使用
事实上,DataInput和DataOutput对应,即DataInput读取由DataOutput写入 的数据.

这里写图片描述
这里写图片描述

import java.io.*;
public class FileWithDataInputStream {
    public static void main(String[] args) {
        FileInputStream fis = null;
        DataInputStream dis = null;
        try{
            fis = new FileInputStream(new File("data.txt"));
            dis = new DataInputStream(fis);
            System.out.println(">>>" + dis.readInt() + "\t" 
                + dis.readDouble() + "\t"
                + dis.readUTF());
        }catch(Exception ex){
            ex.printStackTrace();
        }finally{
            if(dis != null)
                try{
                    dis.close();
                }catch(IOException ex){
                    ex.printStackTrace();
                }   
        }
        if(fis != null)
            try{
                fis.close();
            }catch(IOException ex){
                ex.printStackTrace();
            }
    }
}

缓存的新应用之一就是回推的实现。回推用于输入流,以允许读取字节,然后再将它 们返回(回推)到流中,PushbackInputStream类实现了这一思想,提供了一种机制, 可以“偷窥”来自输入流的内容而不对它们进行破坏

PushbackInputStream类具有以下构造函数:

PushbackInputStream(InputStream inputStream);
/*第一种形式创建的流对象允许将一个字节返回到输入流*/
PushbackInputStream(InputStream inputStream,int numBytes);
/*第二种形式创建的流对象具 有一个长度为numBytes的回推缓存,从而允许将多个字节回推到输入流中*/

PushbackInputStream类还提供了unread()方 法:

void unread(int b)
/*第一种形式回推b的低字节,这会使得后续的read()调用会把这个字节再次读取出来。 */
void unread(byte[] buffer)
/*第二种形式回推buffer中的字节。*/
void unread(byte[] buffer,int offset,int numBytes) 
/*第三种形式回推buffer中从offset开始的numBytes个 字节。当回推缓存已满时,如果试图回推字节,就会抛出IOException异常*/

PushbackInputStream对象会使得InputStream对象(用于创建 PushbackInputStream对象)的mark()或reset()方法无效。

对于准备使用 mark()或reset()方法的任何流来说,都应当使用markSupported()方法进 行检查

程序对应的基本输入为键盘输入,基本输出为显示器输出。Java中, System类的in和out两个成员代表了基本输入输出的抽象 。

System.in:

基本输入,对应InputStream

System.out:

基本输出,对应PrintStream

RandomAccessFile

RandomAccessFile类可以在文件中任何位置查找或写入数据 。

RandomAccessFile同时实现了DataInput和DataOutput接口 。

磁盘文件都是可以随机访问的, 但是从网络而来的数据流却不是。

ByteArrayOutpuStream/ByteArrayInputStream

之前曾经提到过,在Java中最适于用作内存数据缓存的类型是byta[],原 因在于Java中所有能够表达的数据类型占用空间都是byte数据占用空间的 整数倍 。

在之前的输入输出流示例中,我们都将程序的数据源或数据持久化目标谁 定为了磁盘文件,而在很多时候我们的程序运算中间结果只需要将数据缓 存在内存中以便于后续进行传输或最终的持久化 。

ByteArrayOutputStream提供工具将内存中以串行序列存在的流式数据以 一个字节为单位进行切分,形成一个byte[]数组 。

这里写图片描述

import java.io.*;
public class ByteArrayBufferTest{
    public static void main(String[] args) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        DataOutputStream dos = new DataOutputStream(baos);
        try{
            dos.writeInt(12);
            dos.writeUTF("Hello.");
        }catch (Exception e) {
            e.printStackTrace();
        }
        byte[] buf = baos.toByteArray();
        try{
            dos.close();
            baos.close();
        DataInputStream dis = new DataInputStream(new ByteArrayInputStream(buf));
        System.out.println(dis.readInt() + "\t"
            + dis.readUTF());

            dis.close();
        }catch (IOException e) {
            e.printStackTrace();
        }
    }
}

字符输出流

FileInputStram类和FileOutputStream类虽然可以高效率地读/写文件,但 对于Unicode编码的文件,我们需要自行将读取到的字节数据根据编码规 则还原为字符串,因此使用它们有可能出现乱码 。

Writer和OutputStream类似也提供了统一的往流中写入数据的方法,和 OutputStream不同的是,写入数据的单位由字节变成了字符:

abstract void write(char[] cbuf, int off, int len) 

这里写图片描述

FileWriter类称为文件写入流,以字符流的形式对文件进行写操作,其构 造方法有5种重载,以下是常用的几种:

这里写图片描述
这里写图片描述

import java.io.*;
public class FileWriterTest{
    public static void main(String[] args) {
        try{
            FileWriter fw = new FileWriter(new File("File1.txt"),true);
            fw.write("Some Infomation.");
            fw.close();
        }catch (IOException e) {
            e.printStackTrace();
        }
    }
}
FileReader将逐个向文件写入字符,效率比较低下,因此一般将该类对象 包装到缓冲流中进行操作.
import java.io.*;
public class FileWriterTest{
    public static void main(String[] args) {
        try{
            FileWriter fw = new FileWriter(new File("File1.txt"),true);
            BufferedWriter bw = new BufferedWriter(fw);
            bw.write("Other Infomation Faster.");
            bw.close();
            fw.close();
        }catch (IOException e) {
            e.printStackTrace();
        }
    }
}

还可以使用PrintWriter对流进行包装,提供更方便的字符输出格式控制

这里写图片描述

import java.io.*;
public class FileWriterWithPrintWriter {
    public static void main(String[] args) {
        try{
            FileWriter fw = new FileWriter(new File("File2.txt"),true);
            PrintWriter pw = new PrintWriter(fw);
            pw.println("First Line.");
            int i = 325;
            pw.printf("A number:" + i);
            pw.printf("Is Next Line?");
            pw.close();
            fw.close();
        }catch (IOException e) {
            e.printStackTrace();
        }
    }
}

字符输入流

以字符为单位进行数据读取的工具继承自Reader,Reader会将读取到的 数据按照标准的规则转换为Java字符串对象.

字符输入流Reader也提供的统一读取数据的方法(和InputStream不同, 实际开发时更多的调用不同Reader提供的特殊读取方法,如 BufferedReader的readLine(),能够简化操作):

abstract int read(char[] cbuf, int off, int len)

返回值表示本次读 取实际读取到的字 符个数,如果已经 到流的末尾,不能 读取到任何数据, 则返回-1。

FileReader类称为文件读取流,允许以字符流的形式对文件进行读操作, 其构造方法有3种重载方式,以下是常用的几种:
这里写图片描述

与FileWriter相似,该类将从文件中逐个地读取字符,效率比较低下,因 此一般也将该类对象包装到缓冲流中进行操作。

字节流与字符流的适配器

在某些时候虽然我们操作的是字符串,但是不得不面对数据来源是InputStream(字节输入流)的情况,在这种情 况下,Java提供了将InputStream和Reader之间进行转换的工具,事实上,字节输出流和字符输出流之间也存在这 种工具,称为:字节流与字符流的适配器:

InputStreamReader:
字节流通向字符流的桥梁,它使用指定的 charset 读取字节并将其解码为字符。它使用的字符集可以由名称指定或显式给定,或者 可以接受平台默认的字符集。

每次调用 InputStreamReader 中的一个 read() 方法都会导致从底层输入流读取一个或多个字节。要启用从字节到字符的有效转换, 可以提前从底层流读取更多的字节,使其超过满足当前读取操作所需的字节 。

这里写图片描述

import java.io.*;
public class SystemInReader {
    public static void main(String[] args) {
        try{
            BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
            String str = "";
            while(true){
                str = br.readLine();
                System.out.println(str);
            }
        }catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Scanner

Scanner类位于java.util包中,不在java.io包中,不属于IO流 。

Scanner是一个工具类,主要目标是简化文本的扫描,最常使用此类获取 控制台输入,Scanner获取控制台输入的步骤: 。

使用控制台输入创建Scanner对象

Scanner  scanner=new  Scanner(System.in); 

调用Scanner中的nextXXX方法,获得需要的数据类型 • 例如:next、 nextLine、nextInt、nextByte等。

import java.util.*;
public class ScannnerTest {
    public static void main(String[] args) {
        Scanner s = new Scanner(System.in);
        while(true){
            System.out.println(s.nextLine());
        }
    }
}

Java中的输入输出流分为字符流和字节流。字节流继承inputStream和 OutputStream,字符流继承自Reader和Writer。在java.io包中还有许多其 他的流,主要是为了提高性能和使用方便

Java为字节流和字符流提供了转换适配器:InputStreamReader, OutputStreamWriter

对象序列化的作用

Java平台允许我们在内存中创建可复用的Java对象,但一般情况下,只有当JVM处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比JVM的生命周期更长。但在现实应用中,就可能要求在JVM停止运行之后能够保存(持久化)指定的对象,并在将来重新读取被保存的对象。Java对象序列化就能够帮助我们实现该功能。
使用Java对象序列化,在保存对象时,会把其状态保存为一组字节,在未来,再将这些字节组装成对象。必须注意地是,对象序列化保存的是对象的”状态”,即它的成员变量。由此可知,对象序列化不会关注类中的静态变量。
除了在持久化对象时会用到对象序列化之外,当使用RMI(远程方法调用),或在网络中传递对象时,都会用到对象序列化。Java序列化API为处理对象序列化提供了一个标准机制,该API简单易用,在本文的后续章节中将会陆续讲到。

在Java中,只要一个类实现了java.io.Serializable接口,那么它就可以被 序列化

java.io.Serializable是一个标识接口,即意味着它仅仅是为了说明类的可 序列化属性,接口没有包含任何需要子类实现的抽象方法

对象序列化与反序列化

将对象的状态信息保存到流中的操作,称为序列化,可以使用Java提供的 工具ObjectOutputStream. writeObject(Serializable obj)来完成。

从流中读取对象状态信息的操作称为反序列化,可以使用Java提供的工具 ObjectInputStream.readObject()来完成。

对于Serializable反序列化后的对象,不需要调用构造方法重新构造,对象完全以它存储的二进制位作为基础来构造,而不调用构造 方法

对象序列化过程不仅仅保存单个对象,还能追踪对象内所包含的所有引用,并保存那些对象(这些对象也需实现 了Serializable接口)

序列前的对象与序列化后的对象是深复制,反序列化还原后的对象地址与原来的的地址不同,但是内容是一样的, 而且对象中包含的引用也相同。换句话说,通过序列化操作,我们可以实现对任何可Serializable对象的”深度复制“, 这意味着复制的是整个对象网,而不仅仅是基本对象及其引用。对于同一流的对象,他们的地址是相同,说明他 们是同一个对象,但是与其他流的对象地址却不相同。也就说,只要将对象序列化到单一流中,就可以恢复出与 我们写出时一样的对象网,而且只要在同一流中,对象都是同一个

使用默认机制,在序列化对象时,不仅会序列化当前对象本身,还会对该 对象引用的其它对象也进行序列化,同样地,这些其它对象引用的另外对 象也将被序列化,以此类推。所以,如果一个对象包含的成员变量是容器 类对象,而这些容器所含有的元素也是容器类对象,那么这个序列化的过 程就会较复杂,开销也较大。

在现实应用中,有些时候不能使用默认序列化机制。比如,希望在序列化 过程中忽略掉敏感数据,或者简化序列化过程 。

当某个字段被声明为transient后,默认序列化机制就会忽略该字段。

对于上述已被声明为transitive的字段age,除了将transitive关键字去掉之 外,是否还有其它方法能使它再次可被序列化?方法之一就是在Person 类中添加两个方法:writeObject()与readObject().

这里写图片描述

一旦对象被序列化或者反序列还原,就会自动地分别调用者两个方法。也就是说,只要我们提供了这两个方法, 就会使用它们而不是默认的序列化机制 。

这个两个方法必须在类内部自己实现。大家应该注意到这两个方法其实是private类型。也就是说这两个方法仅能 被这个类的其他成员调用,但其实我们没有在这个类的其他的方法中调用这两个方法。那么到底是谁调用这两个 方法呢?是ObjectOutputStream和ObjectInputStream对象的writeObject和readObject()方法分别调用者两个方法 (通过过反射机制来访问类的私有方法),在调用ObjectOutputStream.writeObject()时,会检查所传递的 Serializable对象,利用反射来搜索是否有writeObject()方法。如果有,就会跳过正常的序列化过程,转而调用这 个它的writeObject()方法,readObject方法处理方式也一样 .

在Java中,软件的兼容性是一个大问题,尤其在使用到对象串行性的时候,那么在某一个对 象已经被串行化了,可是这个对象又被修改后重新部署了,那么在这种情况下, 用老软件来 读取新文件格式虽然不是什么难事,但是有可能丢失一些信息。 serialVersionUID来解决这些 问题,新增的serialVersionUID必须定义成下面这种形式:

static final long serialVersionUID=-12345678L; 

其中数字后面加上的L表示这是一个long值。 通过这种方式来解决不同的版本之间的串行话 问题,如果serialVersionUID不同,则defalutReadObject()不能正常执行,即如果类的 serialVersionUID和序列化对象中的serialVersionUID不一致,则说明版本不兼容,不能完成序 列化,这在一些需要强制用户进行版本升级的场景非常重要,因为在这种情况下Java会抛出 InvalidClassException,可以在异常处理中进行升级操作。

如果我们不显式提供serialVersionUID的值,则Java会根据以下几个属性进行自 动计算: • 类的名字
• 属性字段的名字
• 方法的名字
• 已实现的接口
改动上述任意一项内容(无论是增加或删除),都会引起编码值变化,从而引 起类似的异常警报。这个数字序列称为“串行化版本统一标识符”(serial version universal identifier),简称UID。解决这个问题的办法是在类里面新增 一个域serialVersionUID,强制类仍旧使用原来的UID。

JDK中还提供了另一个序列化接口:Externalizable,使用该接口之后,之 前基于Serializable接口的序列化机制就将失效,对象将按照我们自定义 的方式进行序列化或反序列化,这对于一些信息敏感应用或对序列化反序 列化性能要求较高来说非常重要。

Externalizable继承于Serializable,当使用该接口时,序列化的细节需要 由我们自己完成 .

另外,使用Externalizable进行序列化时,当读取对象时,会调用被序列 化类的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值 分别填充到新对象中。由于这个原因,实现Externalizable接口的类必须 要提供一个无参的构造器,且它的访问权限为public.

将对象的状态信息保存到流中的操作,称为序列化,

nio

nio与io的区别

NIO和IO之间最大的区别是:IO是面向流的,NIO是面向块(缓冲区)的

Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节, 它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需 要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需 要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。

Buffer

Java NIO中的Buffer用于和NIO通道进行交互。数据是从通道读入缓冲区, 从缓冲区写入到通道中的 .

缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块 内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。

Channel

Java NIO的通道类似流,但又有些不同: • 既可以从通道中读取数据,又可以写数据到通道。但流的读写通常是单向的

通道可以异步地读写‘

通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入。

Java NIO的事件选择器允许一个单独的线程来监视多个输入通道,你可 以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道: 这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选 择机制,使得一个单独的线程很容易来管理多个通道 。

由于有了事件选择器,因此NIO可以以非阻塞的方式读取数据。
这里写图片描述

完。

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值