Skip to content

Java Basic

HackerOO7 edited this page Aug 7, 2018 · 3 revisions

目录

java 历史

  • 九十年代初,詹姆斯,java前身,Oak,橡树
  • 1994年,Oak 命名为 Java,一种咖啡的名字
  • 1995年,Sun正式对外界公开 Java,并发布 JDK1.0
  • 1996年,Sun 成立了 JavaSoft,使用 java 需申请许可
  • 1998年, Sun 正式将 java 命名为 Java2

Java 创世纪

  • 需要先要安装 JDK
  • 先把 .java 源代码使用 javac 编译为 .class,然后使用 java 运行该 .class

Java 基本概念

  • JRE, Java Runtime Environment
  • Java 虚拟机,JVM, Java Virtual Machine
  • 一次编译,处处运行. 跨平台

java 变量

  • 基本数据类型和引用数据类型
  • 基本数据类型
    • 整型 byte、short、int、long
    • 浮点型 float、double
    • 字符型 char
    • 布尔型 boolean
  • 引用数据类型:class/interface/数组/对象/String
  • 变量命名规范: 以下划线、字母或美元符号开头, 大小写敏感,没有长度限制
  • 驼峰命名法:使用有意义的英文单词,若只有一个单词则所有字母都小写,若有多个英文单词组成则从第二个单词开始首字母大写

java 基本数据类型

  • 浮点类型分为单精度浮点型和双精度浮点型 float、double
  • 字符型 char, 需由单引号括起来,占用两个字节,存储的是 unicode 码,英文的话就是 ASCII 码
  • java 中字符使用 unicode 字符集(Charset),Unicode 字符集为每种语言的每个文字都进行编码,使用十六进制表示,最多允许 1114112 个字符,向下兼容 ASCII,采用两个字节来表示一个字符。因为 Unicode 并未规定最后的二进制如何进行存储,所以有了 UTF-8 UTF-16 等编码方式,UTF-8 UTF-16 都是 Unicode 的一种编码方式(Encoding),规定了二进制的存储
  • UTF-16 是 JVM 内部使用的字符编码方式
  • 虽然 String 是由 char 组成,但在进行读写时依据编码方式的不同,字符串内中英文会占用不同的字节,英文一般都是占用一个字节,而中文就有区别了,因此读写时要注意编码方式
    • GBK,中文占用两个字节,英文一个字节
    • UTF-8,中文占用三个字节,英文一个字节
    • utf-16be 和 utf-16le,中英文各两个字节
    • UTF-16,中英文各占两个字节,若字符串中包含中文,则会默认添加两个字节的前缀来区分大小端
  • byte short int long 分别占用 1 2 4 8 字节,第一个比特位用来表示正负
  • 常量的三种表示形式:十进制直接写、八进制以 0 开头、十六进制以 0x 开头
  • float double 分别占用 4 8 字节
  • 整数字面量为整型(int) 小数字面量为双精度浮点型(double),如果把小数赋值给 float 类型需要在小数后面加上 F,表示小数不再是 double 类型
  • 在四则运算时,运算结果以其中表数范围最大的数据类型为准,如果等号左侧定义的数据类型表数范围小于等号右侧的运算结果类型的范围,则编译报错
  • 表数范围 double > float > long > int > char > short > byte
  • 使用强制类型转换时,如果结果数值范围超出要转换的类型表数范围,有可能在编译时不会报错,但会导致计算出的最后结果是错误的

java 中的编码

  • javac 编译源码文件时,可通过 -encoding utf-8 指定文件的编码格式,以正确读取文件
  • 编译成的 class 文件,统一是 utf-8 编码的,为了更紧凑
  • 运行时 class 文件时,可通过 -Dfile.encoding=utf-8 指定虚拟机运行时的编码方式 Charset.defaultCharset()。代码中的字符/字符串以 utf-16 存储在内存中
  • 若都不指定,则是使用操作系统属性的编码方式读取源文件和运行虚拟机
  • 读取字符/字符串数据时,可指定编码方式,转换为所期望的编码输出
  • 当使用 System.out 输出时,也会将内存中的 utf-16 转换为虚拟机运行时的编码方式进行输出,但有可能和控制台或终端的编码方式不一样,导致乱码
  • 参考

java 运算符表达式

  • 算术运算符 关系运算符 布尔逻辑运算符 位运算符 赋值运算法/扩展赋值运算符 字符串连接运算符
  • 关系运算符的结果是布尔值
  • 逻辑运算符中的 & 和 && 是有区别的,使用 && 或 || 进行逻辑运算时,若前边的条件为 false 或 true, 则 && || 符号后面的条件将不在进行判断,叫做短路。可以使用 i++ 来判断后面的是否执行了
  • 扩展赋值运算符是缩写
  • 强制类型转换的优先级比 . 低,比四则运算符号都要高

位运算符

  • 位运算符主要针对二进制数的每个位进行运算
  • 包括,与、非、或、异或,分别用 &、~、|、^ 符号表示,注意和逻辑运算符的区分
  • 与运算符 &,两个操作数中若位都为1,结果才为1,否则结果为0
  • 或运算符 |,两个操作数中位只要有一个为1,那么结果就是1,否则就为0
  • 非运算符 ,如果位为0,结果是1,如果位为1,结果是0, 是一元操作符,其余都是二元操作符
  • 异或运算符 ^,两个操作数中的位,相同则结果为0,不同则结果为1

java 分支语句

  • if-elseif-else
  • switch
  • switch 只能有四种数据类型 比特型 char short int
  • 如果 switch 中缺少 break,则后面的语句将不在执行判断,都将进行执行,直到下一个 breaks

java 循环语句

  • for 循环
  • for 循环中,i++/++i i--/--i 是没有什么区别的
  • while 循环
  • 循环语句中注意要设定结束条件
  • foreach 结构,list 可以是数组或 ArrayList 对象,: 是指遍历这个对象,类A a是每次循环后所存放数据类型的对象
ArrayList<类A> list=new ArrayList<类A>();
for(类A  a : list){
    操作a;
}

OOP

  • 匿名对象,不进行实例化,直接调用类的方法,new class().funciton()
  • 在同一个类中,多个函数名相同,但参数列表不同,称为函数的重载
  • 函数名和类名相同,并且无返回值定义,被称为构造函数
  • 实例化对象时 class class1 = new class(),实际上就是 new 了一个构造函数, new 的时候该函数会被执行一次
  • 如果类中无构造函数,则编译器在编译时会自动添加一个无参的空的构造函数。若已经写了构造函数,则编译器不会在添加,实例化对象时要对构造函数的参数列表进行赋值

this 关键字

  • this 调用成员变量或成员方法时,this 指的是调用这个方法或变量的当前对象,这个对象就是 this 所在类的实例
  • this(args,args,...); 来调用本类中的其它构造函数,这行代码必须是第一条语句,具体是哪个构造函数取决于传入的参数的个数和类型

static 关键字

  • 使用 static 来定义静态成员变量/静态方法/静态代码块/静态内部类, 还可以用来静态导包
  • 静态成员变量可以直接使用类名来调用,赋值后则所有的对象中该成员变量的值是一样的,使用的是同一块内存块,任何一个对象对该成员变量进行了修改,其它对象的成员变量的值也会得到修改
  • 因为一个 static 成员变量对每个对象来说都只有一份存储空间,存储在方法区中。而非 static 成员变量则是对每个对象都有一个存储空间,存储在堆内存中
  • 用 static 修饰的成员变量是类的成员(一般称为静态变量),是属于类而不必依附于对象的实例化来分配空间(静态变量在类加载的时候就被分配了空间),所以可以使用类名直接调用。而非静态变量(又称实例变量)或实例方法是属于对象的,只有对象被创建之后,才可以被使用
  • 因为静态变量是属于类的,所以 static 不能用于修饰本地变量,只能用来修饰成员变量。不能在方法中定义 static 变量
  • 而 final 关键字是让该存储空间内存块只允许写入一次,例如只能进行一次赋值,赋值后值不再变化,是一个常量
  • 在静态方法中不能够调用非静态方法,只能访问所属类的静态成员变量和静态方法, 同样不能使用 this super 关键字
  • 因为静态方法不依赖于对象,不能调用属于对象的实例变量和实例方法,只能调用方法区中的属于类的东东,如常量、静态变量。或者说静态方法被执行时,在栈里属于该方法的栈帧中方法所属对象的引用指向的是方法区,不是堆内存
  • 静态方法的使用上,可以理解为只是为了方便使用类名来直接调用,除此之外和非静态方法并没有多大区别(除了上面提到的),IDE 或编译器会自动给你报错的
  • 静态代码块 static{...},JVM 在类加载时会执行 static 代码块,但也就是仅仅在加载时执行这一次(除非这个类被 GC 回收后又被加载). 一般在为静态变量赋初始值时使用
  • 静态方法使用 class.fun() 来调用时,注意区别匿名对象使用上的区别

final 关键字

  • final 关键字可以用于成员变量、本地变量、方法以及类
  • 使用 final 声明的类,不允许被继承, String 类就是 final 类
  • final 声明的方法,不可以被子类覆写
  • final 声明的变量,不可二次赋值
  • final 在声明变量时没有赋值的话,必须在构造器中或者调用 this() 初始化,否则编译报错
  • final 修饰的变量有三种:静态变量、实例变量和局部变量,分别表示三种类型的常量
  • final 若声明的变量是基本数据类型并直接给出了其值,而不是通过调用函数赋值,则编译器会把该变量当做常量使用
  • 匿名类和接口中所有变量都是 final
  • 能提高性能,按照代码惯例,final 变量就是常量,而且通常常量名要大写,使用下划线分割各个单词

继承

  • 封装/继承/多态 是面向对象的三个特征
  • 在 OOP 中继承就是一个类得到了另外一个类当中的除私用的成员变量和成员方法
  • java 只支持单继承,不允许多继承,C++支持多继承。单继承就是一个子类只允许有一个父类,多继承指一个子类可以有多个父类
  • 使用 extends 关键字

子类实例化过程

  • 子类无法继承父类的构造函数
  • 在子类的构造函数中,必须调用父类的构造函数,使用 super 关键字,如果没有写,则编译器会默认加上 super() 来调用父类的无参数构造函数,也就是这里子类通过父类的构造函数来继承获取父类的成员
  • 同 this 调用本类中的构造函数一样,具体调用的那个构造函数取决于传递的参数个数和类型,同样这行代码也必须是第一行代码

Override

  • 对父类中的方法进行重写
  • 调用父类中方法,使用 super.fun(..);,然后在进行扩展
  • 使用 this super 关键字,可以在继承中减少重复代码

对象转型

  • 是 OOP 的多态性的一个体现
  • 对象向上转型,是将子类的对象赋值给父类的引用
  • 一个引用能够调用哪些成员(变量和函数),取决于这个引用的类型
  • 一个引用到底调用的哪一个方法,取决于给这个引用赋值的对象
  • 可以这样理解:对象向上转型,就是把子类中覆写的父类方法放到父类中,但对象会遗失和父类不同的方法,就是子类中非覆写的方法会遗失
  • 向下转型,是将父类的对象赋值给子类的引用
  • 向下转型要先经过一次向上转型,然后在经过一次强制转型
  • 向下转型,是把父类的成员放到子类中,相比于向上转型不会遗失子类中非覆写的方法
  • 对象转型的目的是减少重复性代码,增加代码简洁性,如子类对象的引用做为参数传递时

抽象类和抽象函数

  • 只有函数的定义,没有函数体(没有大括号)的函数被称为抽象函数
  • 使用 abstract 关键字,只能用来修饰类或方法,不能用来修饰属性,抽象类不能够实例化对象
  • 一个类中如果有抽象函数,那么这个类必须被声明为抽象类
  • 如果一个类中没有抽象函数,那么这个类也可以被声明为抽象类,主要用于不让这个类实例化对象
  • 抽象类是用来被继承和覆写其中的抽象函数的
  • 抽象类是拥有构造函数的,虽然抽象类不能实例化对象,但可以被子类的构造函数调用
  • 当父类中的一个方法没有统一的实现方式时,必须交由子类来自己实现的话,则这个方法可以使用 abstract,这样继承父类的子类必须覆写该方法,否则编译报错,减少程序内部错误

package

  • 将类放置到一个包当中,需要在这个类的开头添加package package_name;
  • 并且在编译时需要使用-d参数,指定依照包名生成相应文件夹的路径
  • 一个类的全名应该是package_name.class_name,在使用时也按照这样使用
  • 包名命名习惯:包名所有字母应要小写,包名一般情况下是域名倒过来写
  • import 导入类或者包

访问权限控制

  • public,如果类名是 public 权限,则文件名必须和类名相同。两个类在不同的包中的时候,调用另一个类的成员变量或者成员函数的话,则其权限应该为 public 权限
  • private,凡是 private 权限的成员变量或者成员函数,则只能在本类中访问
  • default,什么都不写则是默认权限,只能在同一个包的类互相访问
  • protected,拥有 default 的权限,但是只能修饰成员变量和成员函数,不能修饰类。并且 potected 拥有跨包访问权限,但只能在继承了父类的子类中进行使用,这点有区别于 public
  • 当子类和父类不在同一个包当中时,子类可以继承到父类中的 default 权限的成员变量和成员函数(编译时并不会报错),但是由于权限不够,无法使用(调用时出错)
  • public > protected > default > private
  • 类或者成员变量或成员函数的权限应当尽可能的小,才能体现 OOP 的封装性

interface

  • 接口就是定义好了的标准
  • 使用 interface 关键字修饰定义,可以理解为接口是个比较纯粹的抽象类,接口中的方法全是抽象方法,并且接口当中的所有方法的权限都是 public 或 abstract,就算不写,也都是默认的
  • 接口中的成员变量默认为 public static final, 是常量,需要在初始化时就得赋值
  • implements 实现是一种特殊的继承,顾名思义,因为你要自己实现接口中的所有方法
  • 一个类可以继承实现多个接口,使用 implements 这点和一个子类只能继承一个父类不一样,多个接口使用逗号隔开
  • 一个接口可以继承多个接口,使用 extends 关键字,多个接口使用逗号隔开
  • 因为 implements 是一种特殊的继承,所以也可以向上转型
  • 虽然抽象类和接口不能够实例化对象,但是可以作为引用类型数据来传递,如回调
  • 静态工厂方法模式,就是把许多需要要实例化对象的代码专门放到一个类的方法中进行封装

异常

  • Throwable --> Exception --> RuntimeException,异常类集成关系
  • Throwable --> Error, 程序直接退出
  • runtime exception 以及子类都被称为 unchecked exception,编译时不会报任何错
  • Exception 及其它子类除runtime之外异常被称为 checked exception,在编译时报错未报告的异常
  • checked exception 需要 try...catch...finally 来进行捕获,或使用 throws 关键字来声明
  • 把有可能出问题的代码放到 try 中
  • 对异常的处理关系到代码的健壮性

throw/throws 关键字

  • throw 关键字,抛出异常对象
  • throws 关键字,来声明函数可能产生异常,若声明的是一个 check exception,则调用这个函数时需要使用 try...catch 来捕获

IO

  • 输入流/输出流 字节流/字符流 节点流/处理流
  • 字节流核心类: InputStream/OutputStream, 其子类有 FileInputStream/FileOutputstream
  • InputStram 的核心方法 int read(byte []b, int off, int len),返回值为整型,是真实读取的长度,当读取完成时,read 方法返回 -1
  • OutputStream 的核心方法 void write(byte []b, int off, int len)
  • 字符流核心类: Reader/Writer, 其子类,FileReader/FileWriter,核心方法,int read(char[], int off, int len)/void write(char[], int off, int len)
  • 处理流:BufferedReader,也是字符流核心类的子类,核心方法 readLine 一行一行的读,处理的是字符而不是字节,适用于处理文本文件。返回值为字符串类型,是每行读取到的字符,到达末尾时成返回 null
BufferedReader bufferedreader = new BufferedReader(new FileReader("from.txt"));
或装饰 InputStream
BufferedReader bf = new BufferedReader(new InputstreamReader(in));
  • 装饰者模式(油漆工模式),例如 BufferedReader 就是装饰者,在 new 的过程中,需要给构造函数传递一个对象。处理流来装饰节点流 FileReader,节点流就是被装饰者。装饰者就是给被装饰者添加新功能
  • 字节流和字符流的核心类都是抽象类
  • InputStreamReader 也是字符流Reader的子类,它是字节流到字符流转换的桥梁,可以指定编码格式
  • BufferedInputStream 也是装饰类,是 InputStream 一个子类,实现读取时的缓冲,减少磁盘 IO
  • IO 类中,可以从命名中看出类的用途,如 Buffered*** 一般就是装饰类,提供缓冲机制
  • ByteArrayOutputStream,OutputStream 子类,是一个 byte[] 输出流, 可以把读取到的文件内容放到内存中的一个缓冲区中,以 byte[] 形式,toByteArray() 方法返回我们所需要的 byte[]
  • ByteArrayInputStream,把内存中的一个缓冲区作为 InputStream 使用
  • InputStream 还有一个 read() 方法,每次读取一个字节,返回值为整形是读取到的字节,到达末尾时返回 -1
  • 对象流(ObjectInputStream、 ObjectOutputStream), 在使用 Serializable 接口序列化对象时,把对象转换为对象流,可通过 wirte/readObject 读写对象到文件中

网络访问

  • Socket 和 HTTP 访问网络时,最后都是通过 getInputStream() 得到一个 IO 流,然后对 I/O 流进行操作
  • I/O Socket HTTP 操作,都需要对异常进行捕获和 finally 中关闭连接或读写操作

内部类

  • 内部类,有成员(普通)内部类、静态内部类、匿名内部类、局部内部类
  • 非静态内部类都可以自由使用外部类的成员无论是静态还是非静态的,因为在实例化的过程中已经 new 了一个外部类对象,持有一个外部类的引用,但非静态内部类中不能定义静态成员
  • 实例化普通内部类对象 A.B b = new A().new B()
  • 在非静态内部类中使用 this 指的就是内部类的对象,若要访问外层对象,则需要使用外层 类名.this 来访问
  • 但是静态内部类只能访问外部类的静态成员,并不持有外部类的引用。同静态方法一样,也不可以使用 this/super 关键字,但可以声明静态成员
  • 静态内部类实例化,A.B b = new A.B() 并不依赖于外部类
  • 静态内部类可以放在接口中做为接口的一部分,放到接口中的任何类都自动是 public 和 static 的,也可以显式的声明为 private 的,甚至可以在其内部类中实现外围接口,这并不违反接口的规则
  • 没有类名的内部类被称为匿名内部类,常用于监听器,因为没有名字所以也没有构造函数
  • 一个匿名内部类一定是在 new 的后面,用来继承一个父类或实现一个接口,也因此匿名内部类只有一个实例
  • 局部内部类是定义在一个方法之内的内部类,就像是方法里面的一个局部变量一样,是不能被 public、protected、private 以及 static 修饰的,匿名内部类也是如此。局部内部类和匿名内部类都只能访问方法中定义为 final 类型的局部变量?
  • 此外,普通内部类可以拥有 private、protected、public 访问权限及包访问权限(default),而外部类只能被 public 和包访问两种权限修饰符修饰
  • 但是一个类文件中最多只能有一个类被public修饰,并且这个类的类名必须和文件名相同,若没有 public 类,文件名随便是一个类的类名即可
  • 内部类编译出来的结果就是:外部类$内部类.class
  • 内部类经常使用于监听器,线程等
  • 匿名内部类的使用,b.fun(new A(){...}); 因为 A 是一个接口,所以需要后面的大括号里面要覆写 A 接口的方法。实际上 b 对象的 fun 方法接收的是一个对象,是引用类型数据。程序执行时,先执行 fun 方法,然后才执行覆写的 A 接口的方法
  • 另一种常用的匿名内部类,Class class1 = new Class() {....}; 直接实例化了一个对象
  • 匿名内部类也可结合使用匿名对象,不实例化对象,直接调用对象方法,如实现 Runnable 接口的多线程
new Thread(new Runnable() {
    @Override
    public void run() {
        //逻辑代码
    }
}).start(); 

线程

  • 开发时,比如起一个专门的 UI 线程,下载线程等
  • 创建线程:继承 Thread 类并覆写其中的 run() 方法。 run 方法中的代码被称为线程体,最后生成线程对象,启动线程。但是由于 Java 只支持单继承,因此不能再继承其它类
  • 启动线程要调用 start() 方法,不可直接调用 run() 方法,这样就是一个线程了是顺序执行
  • 线程的生命周期的几种状态:创建、就绪、执行、阻塞、死亡
  • 创建线程第二种方法:实现 Runnable 接口覆写其中的 run() 方法,并将实现类的对象传递给 Thread 构造函数,也就是 new 一个 Thread 对象,并将 Runnable 接口实现类的对象传递过去。然后执行 Thread 对象的 start()。或者采用匿名内部类实现。 方法。这是比较常用的方法,因为还可以继承其它类
  • 中断线程: Thread.sleep() Thread.yield(), sleep 和 yield 是 Thread 类的 static 方法。线程从 sleep 唤醒时会进入就绪状态
  • 线程优先级:最大是10,最小是1,默认是普通优先级5,可以使用 Thread 所提供的静态常量来设置,方法:getPriority() setPriority(). 优先级高的线程,只是被优先执行的概率大而已,不是一定会优先执行的
  • sleep() 方法会让当前线程睡眠指定的时间,线程进入阻塞状态。yield() 线程让步方法,让当前运行线程回到可运行状态,给其它具有相同优先级的线程获得运行机会,但当前线程本身也有可能立即重新被运行
  • synchronized(this){...} 同步代码块,使用同步锁保证代码执行过程中,不被另一个线程执行,其它线程处于阻塞等待状态, 等待同步锁的释放,保证共享数据的安全。this 是指当前调用这个方法的对象
  • synchronized 方法,在方法的声明前加入 synchronized 关键字,声明该方法为同步方法
  • 两个线程先后同时执行同一对象的同步方法或同步代码块时,另一个线程需要等待先前的线程执行完成后,可能继续执行
  • synchronized 锁住的是对象,当一个线程去调用一个对象的同步方法或同步代码块时,这个线程就获得了这个对象的同步锁,那么同个对象上的所有其它有同步锁的代码都不能被另一个线程执行,需要等到同步锁的释放
  • 把 synchronized 关键字放到方法的返回值前面,叫同步方法。同步方法锁住的就是 this, 同步代码块可以明确的指定锁住的对象,一般使用 this,或需要共享的互斥资源的对象
  • wait() notify() notifyAll() 必须用在同步方法或同步代码块中,是 Object 类的方法
  • 调用 wait() 方法的线程会进入阻塞等待状态,并释放该对象上的同步锁,直到其它持有同一对象锁的线程调用 notify()、notifyAll()唤醒或等待超时后恢复运行
  • wait() 和 notify() 方法的使用,this.wait() this.notify(),或者是互斥资源的对象来进行调用,notify() 方法一般放在同步代码块的最后
  • notify() 方法,通知的是一个在同一对象锁上被执行了 wait() 方法后,而处于阻塞等待状态的线程并允许获得锁,执行这个方法并不会真正释放该线程持有的对象锁,只是告诉在同一对象锁上等待的线程可以被唤醒了,直到synchronized代码块执行完才会释放锁
  • nofifyAll() 方法,通知的所有在同一对象锁上被执行了 wait() 方法后的线程去竞争来获得锁
    • wait 的一般用法:
synchronized( lockObject )
{ 
   while( ! condition )
    { 
        lockObject.wait();
    }
 
    //take the action here;
}
- notify 的一般用法:
synchronized(lockObject) 
{
    //establish_the_condition;
 
    lockObject.notify();
 
   //any additional code if needed
}

线程中的异常处理

  • 线程是独立执行的代码片断,线程的问题应该由线程自己来解决,而不要委托到外部
  • 在 Runnable 接口中 run() 方法的声明中,并没有 throws exception
  • 因此针对 run() 方法体中的 checked exception, 编译器强制要求需要进行捕获或声明,但并不能通过关键字 throws 在方法名中进行声明来解决,只能使用 try...catch 进行捕获
  • 但依然可以抛出异常(指 unchecked exception, 因为若是抛出 checked exception 还是会需要 try...catch 进行捕获),抛出时该线程就会终结
  • 但是抛出的异常(显式通过 throw 关键字抛出,或者是真实发生了运行时异常抛出的异常)并不能被捕获,主线程和其他线程完全感知不到这个线程抛出的异常,也就是我们不能捕获从线程中逃逸的异常
  • 出错的信息只会打印在控制台当中,
  • 若要捕获线程中的异常,需要
    1. 实现 Thread.UncaughtExceptionHandler 接口,覆写其中的 uncaughtException() 方法,在该方法中对异常进行处理(打堆栈踪迹,或使用日志 API 将异常报告写入日志文件)
    2. 在线程 start() 之前,通过 thread.setUncaughtExceptionHandler() 方法设置我们实现的未捕获异常处理器 这样,在异常发生时,就会调用我们覆写的 uncaughtException() 方法,对异常进行捕获处理
  • 若使用的是线程池来启动线程,则可以实现 ThreadFactory 接口,覆写其中的 newThread() 方法,通过传入的 Runnable r 参数,创建一个新线程,并设置未捕获异常处理器,最后返回,
Thread t = new Thread(r);
t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
return t;

在实例化线程池的时候,传入 ThreadFactory 的实现类,这样就会给线程池中的每个线程添加一个异常处理器

ExecutorService threadPool = Executors.newCachedThreadPool(new MyThreadFactory());
  • 若并不想使用 ThreadFactory,或者给多个线程挨个设置未捕获异常处理器,可以使用静态方法 setDefaultUncaughtExceptionHandler() 来给所有线程设置一个默认的未捕获异常处理器,
Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler());

这个默认的处理器只有在线程没有单独设置异常处理器时才会调用

  • 如果一个线程没有单独设置异常处理器,此时的异常处理器就是 ThreadGroup 对象,ThreadGroup 类实现了 Thread.UncaughtExceptionHandler 接口,它的 uncaughtException 方法,会做以下处理:
    1. 如果该线程组有父线程组,则调用父线程组的 uncaughtException 方法
    2. 如果 Thread.getDefaultExceptionHandler 方法返回一个非 null 的处理器,则调用该处理器,就是通过 Thread.setDefaultUncaughtExceptionHandler() 设置的处理器
    3. 如果上面都没有的话,最后会把堆栈踪迹输出到控制台上 e.printStackTrace(System.err)
  • 因此,一个线程会优先使用自己单独的异常处理器,若没有,则使用线程组的异常处理器,线程组的异常处理器会判断具体使用哪个处理器
  • 捕获线程中的异常,还可以不使用 Runnable 接口,而使用 Callable 接口,Callable 中的 call() 方法是可以抛出异常,并被外部捕获的。但要注意提交任务的时候使用的是 submit() 方法,该方法返回一个Future对象,所有的异常以及处理结果都可以通过future对象获取
Future<?> future = threadPool.submit(new MyCallable());
try{
    future.get();
} catch(Exception e) {
    e.printStackTrace();
}
  • 但是因为线程抛出异常时会退出,若线程池中线程较多的时候的使用,会增加线程创建的开销。因此建议还是在线程体中使用 try...catch 来捕获可能出现的 unchecked exception

线程中断机制

  • 中断机制是一种协作机制,也就是说通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理
  • interrupt(),方法的作用仅仅是设置中断标识位
  • isInterrupted(),唯一的作用只是检查中断标识位状态,但并不会修改标识位
  • interrupted(),是一个静态方法,检查当前线程的中断状态(并隐式重置为false),换句话说,连续两次调用该方法的返回值必定是false。由于它是静态方法,因此不能在特定的线程上使用,而只能报告调用它的线程的中断状态
  • interrupt() 方法并不能直接中断一个线程的执行,线程是否被中断,还要看线程中对中断的具体响应和处理,当然线程也可以不处理,那么任务将一直执行下去
  • 一般是在线程中使用 isInterrupted() 和 Thread.interrupted() 检查线程中断标识位的状态并做处理,若要终止线程,别忘了 return, 否则线程还是会继续执行
  • 或者捕捉线程中被调用方法抛出的 InterruptedException 来做处理,在被调用的方法中使用 Thread.interrupted() 检查当前线程的状态,并抛出中断异常
  • 在线程体中频繁的检查中断可能会导致程序执行效率低下,较少的检查则可能导致中断请求得不到及时响应,因此实际应用中需要平衡性能和响应灵敏性
  • 但是像 sleep、wait、notify、join 这些方法,本身就可能会抛出 InterruptedException,是因为这些方法会去检查中断标识位,如果中断了,就抛出一个中断异常,并清空中断标志
  • 所以当线程因调用这些方法,而处于阻塞状态时,调用线程的 interrupt() 方法就可以打破阻塞状态,抛出一个中断异常,执行 catch 块中的异常处理代码,如果在 catch 中并没有 return,那么线程就会继续向下执行直到完成退出,这就是说的打破阻塞状态,或唤醒
  • 如何中断一个线程,总结如下:
    1. 在线程中设置一个 flag 标识,在线程体中的 while 循环里通过判断此标识来控制循环是否执行。但如果线程已经处于阻塞状态中,则此方法就无效了
    2. 当线程处于阻塞状态时,针对不同的阻塞类型有不同的方法
    3. 因调用 sleep wait 等方法(会抛出 InterruptedException 的方法)而进入阻塞状态,这种阻塞是可中断阻塞,可以通过 executor.shutdownNow()、thread.interrupt() 来中断,如果线程是通过 submit 方法提交的,可以使用其返回的 Future 对象的 future.cancle() 方法来中断
    4. 因 IO 或等待同步锁的释放而进入的阻塞是不可中断的阻塞,使用以上方法是无法中断线程的. - 针对 IO 而陷入的阻塞,可以通过 close IO流,来中断阻塞。如果使用的是 NIO 的话,就不必要这样了,因为被阻塞的 NIO 通道会自动地响应中断 - 如果同步代码块使用的是 ReentrantLock 的 lockInterruptibly() 方法,则线程因等待锁的释放而进入的阻塞状态是可以被中断的
  • 但如果一个线程运行时只是可能会产生阻塞的话,要想保证这个线程可被百分百中断,就得同时考虑被阻塞和没有阻塞这两种情况。一般固定的写法:
public void run() {
  try{
    while(!Thread.interrupted()){
      // do more work
    }
    // Exiting via while()
  } catch(InterruptedException e) {
      // thread was interrupted during sleep or wait.
      // Exiting via InterruptedException
    } finally {
      // cleanup, if required
    }
}
  • 但是这种写法过于繁琐,更好的选择是封装你的方法,在该方法上声明一个中断异常并在方法中判断中断标识位的状态,如果为 true,就抛出一个中断异常。线程体中调用这个方法时就能捕获该异常

数组

  • 数组用来存储一系列相同类型的数据

  • 数组定义方法,静态定义 动态定义

  • 静态定义方法就是直接声明,每个元素用逗号隔开

  • 动态声明,如 int[] array = new int[length], int 类型的数组默认元素都是0

  • 二维数组,类似数学中的矩阵取值时使用坐标

  • 取出每个之时,使用两层嵌套循环

  • 定义时使用两个中括号

类集框架

  • 主要用于存储和管理对象,位于 java.util 包中,单相对于数组来说,容量是自动扩充的
  • 主要分为三大类:集合(set) 列表(list) 映射(map)
  • 集合:无序不重复,若有重复忽略掉 List:对象按照索引位置排序,可以有重复对象 Map:都是键值对,键不可以重复,而值可以,若键重复,则后面的值覆盖前面的值
  • Set 和 List 接口都是 Collection 接口的子接口,有 add() get() remove() clear() isEmpty() size() 等几个主要方法
  • ArrayList 是 List 接口的实现类,实际上是一个自动扩充的数组
  • LinkedList, 是一个双链表
  • HashSet 是 Set 的实现类, 经常用到把子类实现类向上转型为接口类。
  • 如 ArrayList HashSet 是泛型,特质只存储 String 类型数据
  • 因为 Set 是无序的,取出所有值需要一个迭代器对象
  • 调用 Set 对象的 iterator 方法,会生成一个迭代器对象,该对象用于遍历整个 Set
        //调用 Set 对象的 iterator 方法,会生成一个迭代器对象,该对象用于遍历整个 Set
        Iterator it = set.iterator();

        while (it.hasNext()) {
            System.out.println(it.next());
            
        }
  • HashMap 是 Map 的实现类,有 put get 方法等,具体可看 DASH 中 java 文档
  • HashMap 因为存储的是键值对,所以泛型需要两个类型,如 HashMap<String,String>

线程安全与线程非安全

  • Hashtable 和 synchronizedMap 是线程安全的,它们的方法如get put也都是同步方法,对象锁锁住的是整张表
  • 但是返回的迭代器(Hashtable实际上使用的是Enumeration, 不是Iterator,但并不影响下面的结论)是强一致性迭代器
  • 这种迭代器在遍历时(注意使用 foreach 遍历时,实际上使用的也是迭代器进行遍历), 集合被自身以外的对象修改了(另一个迭代器或者集合本身), 为了保证遍历得到的结果和存储的数据的一致性(视图一致性), 就会抛出 ConcurrentModificationException 错误,这种策略被称为 Fast-fail 策略
  • 这种强一致性迭代器在多线程情况下使用时,若有其它线程在对集合做修改,则需要对遍历方法进行同步,手动获取集合对象上的锁。但这样在多线程中遍历时或 使用 get 方法获取数据时(也就是读的时候),其它线程就不能获取到锁处于阻塞状态,就影响了并发的性能
  • ConcurrentHashMap put 方法也是同步方法,但是因为其采用分段锁,默认允许最多16个线程同时对集合进行写操作,若超过,则处于阻塞状态. 但 get 方法并不是同步方法(弱一致性),读的线程并不做限制, 获取数据时并不加锁,不会阻塞其它线程. 这样就可以实现同时对 map 进行完全并发的读和给定数量的并发的写操作,是性能提升的关键
  • ConcurrentHashMap 返回的迭代器是弱一致性的,并不像强一致性的迭代器一样,遍历时在集合被迭代器自身以外的对象修改后会抛出异常。但弱一致性不一定能够反映出此迭代器被实例化之后集合上所做的数据修改(多线程),也不会抛出异常, 使用时无需进行同步
  • ConcurrentHashMap 的 clear 方法也不是同步方法,也是弱一致性的。弱一致性是为了提高效率,是一致性和效率之间的权衡。如果要保持绝对的一致性,就得使用另两种 map 了
  • ConcurrentHashMap 虽然是弱一致性的,但其实现技术保证 HashEntry 几乎是不可变(通过 final volatile 关键字的使用,但是 remove 时有可能会改变),不允许在链表的中间或尾部添加或删除节点,所有的节点的修改只能从链表头部开始。因此降低了执行读操作的线程在遍历链表期间对加锁的需求(包括 get clear 方法), 而对 value 使用 volatile 关键字,保证了其原子操作和内存可见性,基本上都能获取到最新的值
  • ConcurrentHashMap 不允许将 value 为 null 的值加入,但是在 get 方法中会去判断 value 是否为 null,以此来判断是否有其它线程刚好在对此 HashEntry 做修改,有可能刚好有线程正在添加此节点中或者链表因为删除了某节点而正在重新排列,这时有可能读到的就是 null, 如果是 null 那么就加锁后重新读入这个 value 值。这些策略的互相配合,使得读线程即使在不加锁状态下,也能正确访问获取最新数据
  • 但是,当一个写线程刚好在删除某个节点的同时读线程读这个节点,那么获取到的数据是旧的数据

HashMap 与 Hashtable

  • 当然两者最主要的区别是 HashMap 是线程非安全的;Hashtable 是线程安全的,里面的方法也都是同步方法
  • Hashtable 是不允许空键或空值的;HashMap 是允许空键或空值,但最多只允许一条记录为空键,不允许多条记录为空值;此外 ConcurrentHashMap 允许空键,但不允许空值

equals

  • == 判断引用类型的话,就是判断双方是否指向堆内存中同一个地址,判断基本数据类型就是判断值
  • equals 是 Object 类提供的方法,效果等同于 ==, 之所以可以判断两个 String 对象(引用)的内容是否相等,是因为 equals 方法在 String 类中被覆写
  • 内容相等是指:对象类型相同 两个对象的成员变量的值完全相同
  • 对象类型相同:可以使用 instanceof 操作符进行比较
  • equals 方法通常要针对类和成员进行覆写,一般也要同时覆写 hashCode() 方法

hashCode & toString

  • hashCode() 也是 Object 类提供的方法,也用来鉴定两个对象是否相等,在 Object 中是返回对象在内存中的地址转换成 int
  • HashMap 调用 put get 方法时,会调用 key(注意是key)这个对象的 hashCode() 方法获取一个 hash(看 key 所属泛型类中的 hashCode() 的实现),若此 hash 在 HashMap 的数组中存在,则在调用 key 对象的 equals() 方法,分别判断当前要添加的 key 和该数组元素中链表中的所有的(遍历链表) key 内容是否相同,若有相同的则覆盖原先旧的 value 值,若不相等,则在该链表中添加新的一条映射. 取值是也是如此. 因为不同的 key 有可能会得到相同的 hash, 而每个元素存储着一个 hash, 元素中的链表是所有具有相同 hash 的 key-value 对,HashMap 使用链地址的方法,解决了哈希冲突
  • println 方法,会调用要打印的对象的 toString() 方法,把输出转化为 String 类型
  • 这两个方法都可以在自己的类中去覆写,如将自定义类做为 Map 的 key 的泛型时,根据需求去覆写

Java 字符串

  • String 字符串常量, 创建后不能在修改
  • StringBuffer 字符串变量(线程安全)
  • StringBuilder 字符串变量(非线程安全)
  • 都是 CharSequence 接口的实现类
  • 每次对 String 类型进行改变的时等同于生成了一个新的 String 对象,然后将指针指向该对象
  • 因此对需要经常进行字符串运算时,如拼接、替换、删除,使用 StringBuffer 或 StringBuilder,不会对性能产生影响
  • 字符串常量池或称为字符串缓冲池,当以非 new 的方式创建字符串时,程序会首先在池中寻找相同值的对象,如果存在则指向该对象地址
  • string.intern() 方法是一个 Native 方法,返回值是一个 String 对象。方法的作用是先去常量池中检查是否存在该字符串常量的对象或引用,若存在,则返回。若不存在,在 jdk1.6 中是创建这个字符串常量并返回这个常量对象,在 jdk1.7 中,则是记录原字符串在堆中的引用并返回
  • 在这种实例化的例子中,常量池中会有 a b 两个字符串常量(即双引号括起来的),但是不会有 ab 这个字符串,ab 字符串是在堆中的对象内容里,只有 str1.intern() 后,才会进入到常量池中
String str1 = new StringBuilder("a").append("b").toString();
String str1 = new String("a")+ new String("b"); 
  • 这种情况,因为 String 是不可变类,最后的 s1 引用指向的是一个新的对象,这个对象的内容是 Goodmorning, 并不在常量池中。但如果结合 final 用又不一样了
String s1 = "Good";
s1 = s1 + "morning";
  • StringBuffer 有一 reverse 方法,可实现字符串倒序输出

JVM

  • 这里不仅要从 java 代码去思考,更多的是从编译后的字节码去思考
  • 编译期,是指 javac 把 .java 源文件编译为字节码的过程,.class 格式文件中指定了很多东西,包括编译期间就确定的每个方法在栈帧中的本地变量表大小,在运行期间并不会改变。而堆内存是动态的。在编译期间,还有对常量的转换,代码的一些优化等
  • 运行期间,是指执行 java *.class 后到退出这个过程,包括类的加载、验证、准备、解析、初始化过程
  • Class 文件结构中的常量池用于存放编译期生成的各种字面量和符号引用,这里的字面量是指字符串字面量和声明为 final 的(基本数据类型)常量值,这些字符串字面量除了类中所有双引号括起来的字符串(包括方法体内的),还包括所有用到的类名、方法的名字和这些类与方法的字符串描述、字段(成员变量)的名称和描述符;声明为final的常量值指的是成员变量,不包含本地变量,本地变量是属于方法的。这些都在常量池的 UTF-8 表中(逻辑上的划分)
  • 符号引用,就是指指向 UTF-8 表中向这些字面量的引用,包括类和接口的全限定名(包括包路径的完整名)、字段的名称和描述符、方法的名称和描述符。只不过是以一组符号来描述所引用的目标,和内存并无关,所以称为符号引用

类初始化顺序

  • 可以形象的理解为,类载入子系统在加载和验证、准备、解析阶段(这三个阶段合称连接)完成了这个类从二进制文件或流到 JVM 内存中等待使用的所有材料的准备。如方法区中的运行时常量池是把 Class 文件静态存储结构中的常量池加载到了内存中;方法区的存储的所加载类的类信息,包括了 Class 文件中的其它信息,如方法表、方法代码等。并且在准备阶段,完成对静态变量的内存空间的分配和赋默认值,这些都是在方法区中
  • 然后所有的材料准备完成后,就是初始化了。前边的过程基本都是由虚拟机主导和控制,到了初始化阶段,才真正开始执行类中的代码(或者说是字节码)。举几种典型代码情况:
    1. 使用类名调用静态变量或静态方法时,如果是第一次使用这个类,类载入子系统会先准备所有的“材料”,然后依照父类静态变量静态代码块--子类静态变量静态代码块(假设有父类且没有被初始化,静态变量静态代码块是按照代码顺序执行的)来给静态变量显式赋值和执行静态代码块进行最后的初始化工作,然后就可以调用到静态变量和静态方法了。实际上这里执行的是字节码中的 <clinit>() 指令,并且只会执行一次
    2. 使用 new 来实例化一个对象时,假设这个类已经被加载,则每次实例化一个对象时都会按照父类非静态变量非静态代码块--父类构造函数--子类非静态变量非静态代码块--子类构造函数进行初始化和显示赋值。并在初始化之前就在堆中给对象划分出一块确定大小的空间(虚拟机会将分配到的内存空间除了对象头都初始化为零值),用于存储这些实例变量, 包括从父类继承的实例变量,默认父类中定义的实例变量会出现在子类之前。实际上是执行的 <init>() 指令。如果这个类没有被加载,则需要先执行类构造器 <clinit>()
    3. 在此之前 main 方法所在的类,JVM 会首先对其进行 <clinit>() 初始化(若有的话,比如有静态变量时)
  • 这几种情况都是属于主动引用,会触发类进行初始化。此外还有几种被动引用,并不会触发类的初始化,详见《深入理解Java虚拟机》

常量池

  • 常量池可分为:Class 文件常量池;运行时常量池;字符串常量池
    1. 运行时常量池是方法区的一部分,是一块内存区域,可以说运行时常量池就是用来索引和查找字段和方法名称和描述符的。给定任意一个方法的索引,通过这个索引最终可得到该方法所属的类型信息和名称及描述符信息。一个类加载到 JVM 中后对应一个运行时常量池
    2. Class 文件常量池指的是编译生成的 class 字节码文件,其结构中有一项是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。运行时常量池相对于 Class 文件常量池来说具备动态性,Class 文件常量只是一个静态存储结构,里面的引用都是符号引用。而运行时常量池可以在运行期间将符号引用解析为直接引用
    3. 字符串常量池,在 jdk1.6(含)之前也是方法区的一部分,并且其中存放的是字符串的实例;在 jdk1.7(含)之后,是在堆内存之中,存储的是字符串对象的引用,字符串实例是在堆中;jdk1.8 已移除永久代,字符串常量池是在本地内存当中,存储的也只是引用。字符串常量池是全局的,JVM 中独此一份。运行时常量池中的字符串字面量若是成员的,则在类的加载初始化阶段就使用到了字符串常量池;若是本地的,则在使用到的时候(执行此代码时)才会使用到字符串常量池
  • 其实,“使用常量池”对应的字节码是一个 ldc 指令,在给 String 类型的引用赋值的时候会先执行这个指令,看常量池中是否存在这个字符串对象的引用,若有就直接返回这个引用,若没有,就在堆里创建这个字符串对象并在字符串常量池中记录下这个引用(jdk1.7)。String 类的 intern() 方法还可在运行期间把字符串放到字符串常量池中
  • 此外,除了两种浮点类型剩余的6种基本数据类型的包装类,都使用了缓冲池技术,但是 Byte、Short、Integer、Long、Character 这5种整型的包装类也只是在对应值在 [-128,127]时才可使用缓冲池,超出此范围仍然会去创建新的对象

多态

  • 多态分为两种,一种是编译时多态,通过方法的重载实现;一种是运行时多态,通过对方法的覆写实现
  • 运行时多态的三个必要条件,子类继承父类并覆写了父类的方法,父类引用指向子类对象(向上转型)
  • JVM 在完成类加载后,会将这个 class 文件二进制字节流转化为虚拟机所需格式存储在方法区中,称为类信息。类信息中包含有一个方法表,方法表中包括从父类(一直到 Object 类)继承的所有实例方法(不包含私有方法,因为私有方法不能继承)以及自身覆写的方法的直接引用,这些直接引用指向类信息中相应的方法代码。如果是本类的方法或者是覆写了父类的方法,则指向的是本类类信息中相应的方法代码;如果是父类的方法,则指向的是父类类信息中的方法代码。这样通过方法表中方法的引用就可以访问到该类到根类的所有实例方法
  • 类信息中包括除了上面提到的方法表,方法代码外,还有成员变量的定义等。可以说,类信息就是类文件在运行时的数据结构,包含了该类中所有定义的信息
  • 相同的方法相同的偏移量,是因为方法表中方法的直接引用是从根类按照继承关系依次排列下来的,类自己的方法是排列在最后。因此方法表是实现 Java 多态的一个关键,通过方法表实现了方法的动态绑定
  • 向上转型后会遗失子类中非覆写的方法,是因为引用变量所能调用的方法取决于声明这个引用变量的类型(静态类型,又叫外观类型),而不是创建的对象的类型(实际类型)
  • 重载和覆写说的都是方法,只有类中的方法才有多态的概念,类中的成员变量和内部类并没有多态的概念
  • 静态方法也没有多态的概念,和成员变量取值与父类还是子类一样,都是由声明这个引用变量的类型决定的。也因此可以说静态方法并不能被覆写,也可以从静态方法不是实例方法而是类的方法角度来理解
  • 在子类中对父类的静态方法进行覆写的行为叫做隐藏,因为静态方法并不能被覆写不满足多态的特征,所以隐藏的目的是为了抛弃或者说遮盖父类静态方法

方法调用

  • 方法调用,指的是 JVM 如何确定正确的目标方法,即调用哪一个方法,并得到直接引用
  • JVM 的方法调用指令有四个,分别是 invokestatic,invokespecial,invokevirtual 和 invokeinterface。在这些字节码指令中,要调用的方法都只是一个符号引用
  • invokestatic 、invokespecial 这两个指令是解析调用,是一个静态过程,和多态无关。要调用方法的符号引用在运行时常量池中已经在类加载的解析阶段被解析为了方法的直接引用,就不需要再去方法表中查找了。这些方法有静态方法、私有方法、实例构造器,称为非虚方法。其中 invokestatic 用于调用静态方法,invokespecial 用于调用私用方法和实例构造器。此外虽然被 final 修饰的方法是使用 invokevirtual 来调用的,但由于 final 方法无法被子类覆写,只存在唯一版本,所以也是一种非虚方法。这也是这些方法无法被继承或覆写在 JVM 中的体现
  • invokevirtual 和 invokeinterface,才是具有多态性的。又分为上面提到的编译时多态和运行时多态,编译时的多态是静态分派,运行时多态是一个动态绑定过程,是动态分派。这两种分派都是通过方法的符号引用去运行时常量池中去查找得到方法所属类型(即调用该方法的引用变量所属类型,是静态类型)及方法名和描述符,然后根据方法名和描述符去所属类型的方法表中查找确定该方法
    1. 如果静态类型是和实际类型相同,那么到此就可得到目标方法的直接引用,这就是一个静态分派的过程。之所以称为静态分派(Method Overload Resolution),是因为在编译成的字节码文件中就通过方法的符号引用、Class 文件常量池、方法所属 Class 文件中的方法表层层解析到了目标方法。因此对于重载,编译时就已经确定好了,虚拟机运行时是不会去管它的,因而体现不了动态性,也可以说不算多态
    2. 如果静态类型是和实际类型是不同的(父类引用指向子类对象,父类是静态类型,子类是实际类型),那就要在运行期间根据实例类型确定目标方法执行版本的分派,称为动态分派。因为实际类型在运行期间可随着程序变化,因此只能在在运行期间才能确定一个对象的实际类型是什么,编译时编译器并不知道,只知道静态类型是什么。那就需要动态的分派目标方法执行版本。则针对 invokevirtual 和 invokeinterface 的具体过程是不同的
    • invokevirtual,实例调用,调用对象的实例方法。动态的分派目标方法执行版本是先在父类方法表中找到目标方法并得到偏移量,根据桟中对象的引用得到这个对象,在根据对象中指向方法区类信息和运行时常量池的指针得到对象的实际类型的类信息,然后在得到的类信息方法表相同偏移量的位置查找目标方法。如果子类覆写了父类的这个方法,则这个方法就是目标方法执行版本,取得这个直接引用;如果子类并没有覆写这个方法,那么这个方法的直接引用指向的是父类的方法代码
    • invokeinterface, 接口方法调用,这里是接口的引用指向的是实现类的对象。因为一个类可以实现多个接口,所以就不能按照偏移量去实现类的方法表中查找了,只能通过搜索完整的方法表
  • 所有依赖静态类型来定位方法执行版本的分派动作是静态分派,而动态分派是在运行期根据实际类型来确定方法执行版本的分派过程

字段调用

  • 字段调用和方法调用类似,有 getstatic getfield
  • getstatic 获取静态变量的值,调用的字段符号引用在运行时常量池中已经被解析成了直接引用,可直接取的其值
  • getfield 获取实例变量的值,首先在运行时常量池中通过调用的字段符号引用获取这个字段的所在类(即调用该字段的引用变量所属类型,是静态类型)及字段名和数据类型, 然后从引用所指向的对象中查找这个字段并取得直接引用。因为字段并没有覆写多态之说,而且对象中存储的实例变量包括从父类继承下来的,因此,取值与父类还是子类字段实例变量的值,取决于静态类型

IDE

  • 自动生成代码
  • 代码重构

附录

概念

  • 字段 field,又称为数据成员,指的是成员变量
  • 本地变量,有的称为局部变量
  • 方法签名是方法名和参数列表的合称
  • 方法描述符,是指参数信息(参数个数,参数类型,参数顺序)和返回值类型,方法所在类
  • 字段描述符,是指字段所在类,字段数据类型

链式调用

  • Identifier.method1().method2().[methodn_1()].methodn(), Identifier——可以是对象,或者类。如果是类,则后面的 method1 就是静态内部类或静态方法,如果 method1 是静态内部类,则 method3 就是该静态内部类中的静态方法,后面的就都是方法了。如果 method1 是静态方法,就比较像单例模式了。如果 Identitier 是对象,则后面的 method 就都是方法了
  • 如果最前面加上 new,则是构建器模式。Identifier 就是个类,method1 就其静态内部类,method2 就是静态内部类中的方法。如 Android 中 OkHttp 中 Request 的对象实例化,AlertDialog 的实例化,用的都是构建器模式
  • 此外,还有如匿名对象 new class().fun()
You can’t perform that action at this time.