面向对象
基本概念
对象:将问题空间中的元素以及它们在解决方案空间的表示称作“对象”
- 属性: 用成员变量表示
- 行为:用成员方法表示
类:同种物体在属性和行为上的集合与抽象。
- 属性:描述对象有哪些属性 ——通过定义成员变量
- 行为:描述对象有哪些行为,以及行为的具体表现 ——通过定义成员方法
类和对象的关系
类是用来描述对象的
- 描述对象有哪些属性
- 描述对象有哪些行为
对象是具体的事物;类是对对象的抽象
类可以看成一类对象的模板,对象可以看成该类的一个具体实例。
类是用于描述同一类型的对象的一个抽象概念,类中定义了这一类对象所应具有的共同的属性、方法。
对象和对象的关系
不同类的对象——具有不同的属性和行为
同种类型的对象——对象具有相同的属性,但是属性的取值可以各不相同
对象的创建及使用
- 创建对象,首先要定义类,描述对象有哪些属性和行为
- 定义好类之后,就可以使用 new 关键字,创建该类的对象
- 对象的使用,即访问对象的成员,通过对象名. 的形式
public class BasicDemo2 {
public static void main(String[] args) {
//创建对象 描述张三这个学生
Student zhangsan = new Student();
//使用对象,指的是访问对象
zhangsan.name = "zhangsan";
zhangsan.age = 18;
zhangsan.isMale = true; //男生
zhangsan.sno = 1;
}
}
/*
定义学生类:描述所有的学生对象
*/
class Student {
//定义学生对象所具有的属性
//姓名
String name;
//年龄
int age;
//性别
boolean isMale;
//学号
int sno;
//定义学生对象所具有的行为
//吃饭
public void eat() {
System.out.println("eating");
}
//睡觉
public void sleep() {
System.out.println("sleeping");
}
//学习
public void study() {
System.out.println("好好学习,天天向上");
}
}
从数据类型角度理解类和对象
类:
定义了一组数据集合(成员变量)和基于这个数据集合的一组操作(成员方法)
是一种自定义类型
对象:
自定义数据类型的一个“变量”
类和对象的内存映像
类信息存储在方法区中,并且包含了每个方法实现的字节码指令
对象存储在堆上,对象中只存储了成员变量的值
特殊语法
成员变量 VS 局部变量
定义的位置不同:
成员变量定义在方法体之外,局部变量定义在类体中或者是方法的形式参数
内存位置不同:
成员变量的值存储在堆上,局部变量的值在栈上
生命周期不同:
成员变量的存储空间随对象的销毁而销毁,局部变量随方法栈帧的销毁而销毁
初始值不同:
局部变量没有被自动赋予默认初值,而成员变量会有默认初值
方法引用类型的形式参数
实参和形参的值传递方式,是复制传递
引用类型的变量,在调用方法,和被调用方法中,都属于各自方法中定义的局部变量
引用类型的实参和形参的值,在调用方法和被调用方法中各自都有一份
但是,引用类型的实参和形参所指向的数据,是同一份数据
public class User4 {
int id; //id
String name; //账户名
String pwd; //密码
public User4(int id, String name) {
this.id = id;
this.name = name;
}
public void testParameterTransfer01(User4 u){
u.name="魏小五";
}
public void testParameterTransfer02(User4 u){
u = new User4(200,"魏三");
}
public static void main(String[] args) {
User4 u1 = new User4(100, "魏小五");
u1.testParameterTransfer01(u1);
System.out.println(u1.name);
u1.testParameterTransfer02(u1);
System.out.println(u1.name);
}
}
构造方法
作用
在创建对象的时候,完成对象成员变量的初始化工作
语法
方法名固定,必须和类名相同
方法声明中,没有返回值类型这一项
其他和普通的方法定义类似
[修饰符] 类名(形参列表){
//n条语句
}
注意事项
- 构造方法可以重载,重载条件和普通方法的重载条件相同,可以根据new类名(实参列表)中的实参列表指明创建某对象时所使用的的构造方法
- 如果没有定义任何构造方法,则 jvm 会自动帮我们添加一个默认构造方法;否则,不会
- 构造方法的执行时机:创建对象的最后一步,jvm用来初始化对象中成员变量的值
this
表示当前对象的引用
如何确定this指的是哪个对象?
如果this出现在,构造方法中,指的是正在创建的那个对象
如果this出现在成员方法中,因为成员方法总是以对象名的方式访问,使用对象访问该方法,this就指的是那个对象
this的作用
解决成员变量的隐藏问题
访问对象的成员变量和成员方法
访问对象的构造方法
public class User {
int id; //id
String name; //账户名
String pwd; //密码
public User() {
}
public User(int id, String name) {
System.out.println("正在初始化已经创建好的对象:"+this);
this.id = id; //不写this,无法区分局部变量id和成员变量id
this.name = name;
}
public void login(){
System.out.println(this.name+",要登录!"); //不写this效果一样
}
public static void main(String[] args) {
User u3 = new User(101,"和二");
System.out.println("打印和二对象:"+u3);
u3.login();
}
}
static
特点
被该类的所有对象所共享(判定是否使用static的关键)
可以直接通过类名访问static修饰的成员变量和成员方法
随着类加载而加载:
- static修饰的成员变量,类加载时分配存储空间,并赋予默认初值
- static修饰的成员方法,类加载完毕,就可以通过类名访问
优先于对象而存在
注意事项:
- 静态方法中不能访问非静态的成员变量和非静态的成员方法
- 静态方法中不能使用this
- 不管静态方法还是非静态方法中,都不能使用static关键字
被static修饰的成员变量的存储位置:
方法区中
静态成员变量 VS 普通成员变量
所属不同
内存中的位置不同
内存中出现的时间不同
访问方式不同
代码块
定义
在Java中,使用 {} 括起来的代码被称为代码块
局部代码块
定义在方法体中,通常开发中没人使用
//局部代码块
public void localCodeBlock() {
//嵌套的作用域中,不能定义和外层作用域中同名的变量
//int a = 100;
//局部代码块 开发中没人这么用
{
int a = 10;
System.out.println("这就是一个局部代码块");
System.out.println(a);
}
//变量a,超出了作用域,所以访问不到
//System.out.println(a);
}
构造代码块
定义位置
定义在类中方法体之外
执行时机
每次创建对象的时候,而且先于构造方法执行
注意事项
如果有多个构造代码块,则这多个构造代码块,按照书写的先后顺序执行
{
//在构造代码块中,可以访问成员变量
i = 100;
System.out.println(i);
System.out.println("这是第一个构造代码块");
//可以访问
// 公共代码
//System.out.println("很多公共代码");
}
静态代码块
定义位置
类中方法体之外,但同时代码块被static关键字修饰
执行时机
随类加载而执行
注意事项
- 静态代码块,属于静态上下文,在静态代码块中,不能访问非静态的成员变量和成员方法
- 因为随着类加载而加载,所以同一个jvm中,只会执行一次
static {
// 静态代码块中不能访问非静态的成员变量或者成员方法
// System.out.println(i);
// localCodeBlock();
//静态代码块中可以访问静态成员变量或者静态成员方法
System.out.println("这是静态代码块");
}
同步代码块
(多线程的时候学习)
package
作用
- 类比于操作系统中用来组织文件的文件夹,java语言中,用包来组织类
- 包还可以用来解决,类的同名问题,不同包下可以存在同名类
语法
package关键在后跟包名,且这条语句必须放在java文件第一条语句的位置
包的命名方式
域名反转的方式
注意事项
当一个java文件中,没有package关键字定义类所在的包时,类所属的包是默认包
import关键字
作用
- 当在类体中使用了与当前类不同包的类名时,编译器编译因为无法找到该类的定义而失败
- 使用import声明,为编译器提供该类的定义信息
语法
- import语句后跟累的全限定名
- 类的全限定名是指包名+类名,在java语言中,包名+类名才能唯一确定一个类
注意事项
- import声明一般紧跟在package声明之后,必须在类声明之前
- java语言核心包 java.lang 包中的类将被隐式导入,可以直接使用其中的类
- 可以使用import<包名>.*;一次导入一个包下的多个类,但是这种方式不会自动嵌套导入子包中的类
访问权限
修饰符 | 同一个类 | 同一个包中 | 子类中 | 所有类 |
---|---|---|---|---|
private | * | |||
default | * | * | ||
protected | * | * | * | |
public | * | * | * | * |
-
private 表示私有,只有自己类能访问
-
default表示没有修饰符修饰,只有同一个包的类能访问
-
protected表示可以被同一个包的类以及其他包中的子类访问
-
public表示可以被该项目的所有包中的所有类访问
public: 接口访问权限
当你使用关键字 public,就意味着紧随 public 后声明的成员对于每个人都是可用的,尤其是使用类库的客户端程序员更是如此。假设定义了一个包含下面编译单元的 dessert 包:
// hiding/dessert/Cookie.java
// Creates a library
package hiding.dessert;
public class Cookie {
public Cookie() {
System.out.println("Cookie constructor");
}
void bite() {
System.out.println("bite");
}
}
记住,Cookie.java 文件产生的类文件必须位于名为 dessert 的子目录中,该子目录在 hiding 下,它必须在 CLASSPATH 的几个目录之下。不要错误地认为 Java 总是会将当前目录视作查找行为的起点之一。如果你的 CLASSPATH 中没有 .
,Java 就不会查找单独当前目录。
现在,使用 Cookie 创建一个程序:
// hiding/Dinner.java
// Uses the library
import hiding.dessert.*;
public class Dinner {
public static void main(String[] args) {
Cookie x = new Cookie();
// -x.bite(); // Can't access
}
}
输出:
Cookie constructor
你可以创建一个 Cookie 对象,因为它构造器和类都是 public 的。但是,在 Dinner.java 中无法访问到 Cookie 对象中的 bite()
方法,因为 bite()
只提供了包访问权限,因而在 dessert 包之外无法访问,编译器禁止你使用它。
默认包
以下代码尽管看上去破坏了规则,但是仍然可以编译:
// hiding/Cake.java
// Accesses a class in a separate compilation unit
class Cake {
public static void main(String[] args) {
Pie x = new Pie();
x.f();
}
}
输出:
Pie.f()
同一目录下的第二个文件:
// hiding/Pie.java
// The other class
class Pie {
void f() {
System.out.println("Pie.f()");
}
}
最初看上去这两个文件毫不相关,但在 Cake 中可以创建一个 Pie 对象并调用它的 f()
方法。(注意,CLASSPATH 中一定得有 .
,这样文件才能编译)通常会认为 Pie 和 f()
具有包访问权限,因此不能被 Cake 访问。它们的确具有包访问权限,这是部分正确。Cake.java 可以访问它们是因为它们在相同的目录中且没有给自己设定明确的包名。Java 把这样的文件看作是隶属于该目录的默认包中,因此它们为该目录中所有的其他文件都提供了包访问权限。
private: 你无法访问
关键字 private 意味着除了包含该成员的类,其他任何类都无法访问这个成员。同一包中的其他类无法访问 private 成员,因此这等于说是自己隔离自己。另一方面,让许多人合作创建一个包也是有可能的。使用 private,你可以自由地修改那个被修饰的成员,无需担心会影响同一包下的其他类。
默认的包访问权限通常提供了足够的隐藏措施;记住,使用类的客户端程序员无法访问包访问权限成员。这样做很好,因为默认访问权限是一种我们常用的权限(同时也是一种在忘记添加任何访问权限时自动得到的权限)。因此,通常考虑的是把哪些成员声明成 public 供客户端程序员使用。所以,最初不常使用关键字 private,因为程序没有它也可以照常工作。然而,使用 private 是非常重要的,尤其是在多线程环境中。
以下是一个使用 private 的例子:
// hiding/IceCream.java
// Demonstrates "private" keyword
class Sundae {
private Sundae() {}
static Sundae makeASundae() {
return new Sundae();
}
}
public class IceCream {
public static void main(String[] args) {
//- Sundae x = new Sundae();
Sundae x = Sundae.makeASundae();
}
}
以上展示了 private 的用武之地:控制如何创建对象,防止别人直接访问某个特定的构造器(或全部构造器)。例子中,你无法通过构造器创建一个 Sundae 对象,而必须调用 makeASundae()
方法创建对象。
任何可以肯定只是该类的"助手"方法,都可以声明为 private,以确保不会在包中的其他地方误用它,也防止了你会去改变或删除它。将方法声明为 private 确保了你拥有这种选择权。
对于类中的 private 属性也是一样。除非必须公开底层实现(这种情况很少见),否则就将属性声明为 private。然而,不能因为类中某个对象的引用是 private,就认为其他对象也无法拥有该对象的 public 引用
protected: 继承访问权限
要理解 protected 的访问权限,我们在内容上需要作一点跳跃。举个使用 protected 的例子。
关键字 protected 处理的是继承的概念,通过继承可以利用一个现有的类——我们称之为基类,然后添加新成员到现有类中而不必碰现有类。我们还可以改变类的现有成员的行为。为了从一个类中继承,需要声明新类 extends 一个现有类,像这样:
class Foo extends Bar {}
类定义的其他部分看起来是一样的。
如果你创建了一个新包,并从另一个包继承类,那么唯一能访问的就是被继承类的 public 成员。(如果在同一个包中继承,就可以操作所有的包访问权限的成员。)有时,基类的创建者会希望某个特定成员能被继承类访问,但不能被其他类访问。这时就需要使用 protected。protected 也提供包访问权限,也就是说,相同包内的其他类可以访问 protected 元素。
回顾下先前的文件 Cookie.java,下面的类不能调用包访问权限的方法 bite()
:
// hiding/ChocolateChip.java
// Can't use package-access member from another package
import hiding.dessert.*;
public class ChocolateChip extends Cookie {
public ChocolateChip() {
System.out.println("ChocolateChip constructor");
}
public void chomp() {
//- bite(); // Can't access bite
}
public static void main(String[] args) {
ChocolateChip x = new ChocolateChip();
x.chomp();
}
}
输出:
Cookie constructor
ChocolateChip constructor
如果类 Cookie 中存在一个方法 bite()
,那么它的任何子类中都存在 bite()
方法。但是因为 bite()
具有包访问权限并且位于另一个包中,所以我们在这个包中无法使用它。你可以把它声明为 public,但这样一来每个人都能访问它,这可能也不是你想要的。如果你将 Cookie 改成如下这样:
// hiding/cookie2/Cookie.java
package hiding.cookie2;
public class Cookie {
public Cookie() {
System.out.println("Cookie constructor");
}
protected void bite() {
System.out.println("bite");
}
}
这样,bite()
对于所有继承 Cookie 的类,都是可访问的:
// hiding/ChocolateChip2.java
import hiding.cookie2.*;
public class ChocolateChip2 extends Cookie {
public ChocoalteChip2() {
System.out.println("ChocolateChip2 constructor");
}
public void chomp() {
bite(); // Protected method
}
public static void main(String[] args) {
ChocolateChip2 x = new ChocolateChip2();
x.chomp();
}
}
输出:
Cookie constructor
ChocolateChip2 constructor
bite
尽管 bite()
也具有包访问权限,但它不是 public 的。
包权限
public:
被public修饰的类,同包或非同包都可以访问到,比如不管同包非同包,都可以创建该类的对象。
默认(缺省):
默认权限的类,只有同包中的类可以访问,比如在同包其他类中,创建类对象。
面向对象三大特征
封装
概念
是指将数据和基于数据的操作封装在一起
好处
- 数据被保护在内部
- 系统的其他部分只有通过在数据外面的被授权的操作才能够进行交互(结合访问权限理解)
- 目的在于将类使用者class user和类设计者class creator区分
在代码中的体现
- 在定义成员变量和成员方法的时候,要考虑,通常只赋予其最小的访问权限
- 如果用private修饰成员变量,通常还要考虑提供对应的get,set方法
class Person {
private String name;
private int age;
public Person() {
}
public Person(String name, int age) {
this.name = name;
// this.age = age;//构造方法中不能直接赋值,应该调用setAge方法
setAge(age);
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setAge(int age) {
//在赋值之前先判断年龄是否合法
if (age > 130 || age < 0) {
this.age = 18;//不合法赋默认值18
} else {
this.age = age;//合法才能赋值给属性age
}
}
public int getAge() {
return age;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}
public class Test2 {
public static void main(String[] args) {
Person p1 = new Person();
//p1.name = "小红"; //编译错误
//p1.age = -45; //编译错误
p1.setName("小红");
p1.setAge(-45);
System.out.println(p1);
Person p2 = new Person("小白", 300);
System.out.println(p2);
}
}
继承
基本知识
含义
- 表示java中的一种代码复用机制
- 表示子类与父类两种数据类型之间的一种继承或者说从属关系,或者说is a 的关心
- 语法: class 子类名 extends 父类名{}
相关概念
子类也叫派生类或导出类
父类也叫超类,或基类
优缺点
优点
- 代码复用,复用已有的类定义代码
- 提高了代码的可维护性,父类中的代码可以被多个子类复用,被复用代码只定义在父类中,只有一份
- 弱化了java语言中的类型约束,父类类型的引用可以指向子类类型的实例
缺点
父类的修改可能会出现在所有子类中(我们无法选择这些修改可以反应在哪些子类中,不反应在哪些子类中)
java语言中继承的特定
单重继承 extends关键词后面只能跟一个类名
注意事项
- 子类只能访问父类所有非私有的成员(成员方法和成员变量)
- 子类不会继承父类的构造方法
子类对象的初始化
研究的问题
子类对象中,现有两部分数据:
-
父类中声明的成员变量的值
-
子类中自己定义的成员变量的值
这两部分数据,初始化(赋初值)的一个先后顺序问题
先后顺序的规定
java语言规定
必须先初始化父类成员变量的值,在初始化子类成员变量的值
如何理解这个顺序
- 子类继承父类,父在先,子在后,父类的成员变量先初始化,然后才是子类自己定义的成员变量值的初始化
- 初始化子类成员的的时候,子类成员变量的初值,和父类成员变量的值有关系
如何实现
保证父类的构造方法先执行,子类的构造方法后执行
两种方式实现
- 隐式初始化(由jvm保证先初始化父类成员)
- 父类中存在默认构造方法
- 子类构造方法在执行时没有显示的调用父类的其他构造方法
- 则子类构造方法执行前,会自动调用父类的默认构造方法
- 显示初始化(由我们自己写代码保证)
- 我们自己在子类构造方法中通过super(实参列表) 的方式调用父类构造方法
- super语句必须处在子类构造方法的第一条语句的位置
- 若父类没有默认构造方法,子类构造方法必须在第一条语句的位置,通过super调用父类某个构造方法
super关键字
super代表对象的内存空间的标识,也可以理解为父类对象的引用
作用
- 访问父类对象的成员变量值
- 访问父类对象的成员方法
- 调用父类的构造方法
域的隐藏问题
- 子类中是可以定义和父类成员变量,同名的成员变量
- 如果,子类中定义了和父类中成员变量同名的成员变量
- 子类类体中定义的方法,通过变量名访问到的是子类自己定义的成员变量的值
- 父类类体中定义的方法,通过变量名,访问到的是父类中定义的成员变量的值
- 在子类类体中,可以通过super.变量名,访问到父类对象的同名成员变量的值
方法覆盖
-
子类中可以定义和父类一模一样的方法
-
如果子类中定义和父类一模一样的方法
- 在子类类体中,调用该方法,调用的是子类中定义的方法
- 在父类类体中,调用该方法,调用的也是子类中定义的方法
-
在子类类体中,可以通过super.方法,访问到父类中定义的那个和子类相同的方法
-
方法覆盖的条件
- 方法的权限修饰符
- 子类方法的权限不能小于父类方法的权限
- 方法返回值类型
- 子类方法返回值类型和父类方法返回值类型相同
- 子类方法返回值类型,是父类方法返回值的子类类型
- 方法签名
- 方法名相同
- 方法的形参列表相同
- 方法的权限修饰符
-
方法覆盖的作用
在子类类体中,修改父类中定义方法的实现(注意并没有在父类中去修改父类中的方法实现)
-
注意事项
- 父类的私有方法不能被子类覆盖
- 静态方法不能被覆盖
多态
“同一个对象”的行为,在不同的时刻或条件下,表现出不同的效果。(这里的同一个对象之所以打引号,其实指的是同一个引用变量)
多态的前提条件
- 继承
- 方法覆盖
- 父类引用指向子类实例
成员的访问特性
- 成员变量
编译看左边,运行看右边
- 成员方法
编译看左边,运行看左边
- 理解
-
成员变量
成员变量值,作为对象的属性值,描述对象的外貌
多态中,父类引用指向子类实例,相当于给子类对象披上了父类的外衣
因此,子类对象的外貌,看起来就是父类对象,因此多态成员变量的访问特征是编译看左边,运行看右边
-
成员方法
虽然看起来,是父类对象的样子,但是因为,父类引用实际指向的是一个子类对象
因此,其行为,表现的是子类对象的行为
多态的好处
- 提高了程序的维护性(由继承保证)
- 提高了代码的扩展性(由多态保证)
多态的弊端
-
不能访问子类特有功能(因为父类引用指向子类实例)
-
解决:
-
instanceof
判断对象所属类型是否是目标类型
-
强制类型转换
子类引用——>父类引用(向上转型)
父类引用 ——> 子类引用(向下转型)
-
final关键字
修饰类
被final修饰的类不能被其他类继承
修饰方法
被final修饰的方法不能被覆盖
修饰变量
修饰成员变量
对象创建完毕之前前被赋值,且仅能被赋值一次
修饰局部变量
使用之前必须被赋值,且仅能被赋值一次
抽象类
概念
包含有抽象方法的类或被abstract修饰的类
注意事项
-
抽象类和抽象方法必须用abstract关键字修饰
abstract class 类名 {}
public abstract void eat();
-
抽象类不一定有抽象方法,有抽象方法的类一定是抽象类
抽象类特征
抽象类不能直接实例化 ,只能间接实例化
抽象类的子类可以是抽象类,也可以是具体类(当子类是具体类时,必须实现父类中所有的抽象方法)
抽象类的组成特征
构造方法
-
和普通类相同
-
抽象类不能直接实例化,还有构造方法的原因
- 抽象类中可以定义成员变量
- 为了能在使得子类,方便初始化抽象父类中成员变量的值
成员变量
- 同普通类
成员方法
- 既可以有抽象方法,也可以有非抽象方法
和abstract冲突的关键字
- private
- static
- final
接口
概念
-
表示一组特殊功能的集合(往往只包含这组特殊功能的声明)
-
在java语言中,接口interface和类处于同等地位都表示数据类型
-
类和接口的对比
- 类定义了一个数据集合(成员变量)和基于这个数据集合的一组操作(成员方法),操作之间有一定联系(操作同一个数据集合)
- 接口中通常只包含方法声明,即接口中的方法都是抽象方法(jdk8之前)
- 接口中声明的方法,就纯粹表示一组功能的集合,功能相互之间可以没有啥联系,比较松散
语法
接口用关键字interface表示
格式:interface 接口名 {}
类和接口的关系
类可以实现接口,类实现接口用implements表示
实现关系,其实是一种实质性的继承关系
接口特征
接口不能直接实例化,只能间接实例化
接口的子类,可以是抽象类也可以是具体类 (具体类必须实现接口中的所有抽象方法)
接口的组成特征
无构造方法
只能是常量,默认修饰符public static final
成员方法:只能是抽象方法,默认修饰符public abstract (jdk8之前)
接口实现了java语言的多重继承
接口与接口之间可以是多重继承
一个类可以在继承另一个类的情况下,实现多个接口(实现关系,也是一种实质上的继承关系)
抽象类 VS 接口
成员区别
-
成员变量
- 抽象类的成员变量,可以是变量可以是常量
- 而接口中的成员变量,只能是常量
-
成员方法
- 抽象类的成员方法,可以是抽象方法,也可以是非抽象方法
- 接口中的成员方法,只能是抽象方法(jdk8之前)
关系的区别
-
类与抽象类
- 继承关系,而且是单重继承
-
类与接口
- 实现关系,一个类可以实现多个接口
-
接口与接口
- 继承关系,接口与接口之间可以实现多重继承
设计理念的区别
- 抽象类 被继承体现的是:”is a”的关系。共性功能
- 接口 被实现体现的是:"like a”的关系。扩展功能
JDK8中引入的特殊方法
默认方法
- 在接口中定义的非静态的,可以有方法体的方法
- 而且在接口中添加默认方法,不会影响实现接口的子类
- 默认方法可以被子类覆盖
静态方法
- 接口中定义的,可以有方法体的静态方法
- 只能在定义静态方法的接口,或者通过定义接口的接口名.静态方法(),的方式访问
内部类
概念
定义在其他类内部的类就称为内部类
访问特征
内部类可以直接访问外部类的成员,包括私有。
外部类要访问内部类的成员,必须创建对象。
按照内部类在类中定义的位置不同
成员位置内部类
定义在类中,方法体之外,可以看做是外部类的一个普通成员,这意味着,内部类依赖于外部类对象而存在
创建内部类对象
外部类中,和创建普通类对象没啥区别
外部类的外部
- 外部类名.内部类名 对象名 = 外部类对象.内部类对象
成员内部类常用修饰符
private
- 保证成员位置内部类只对其外部类可见
static
- 整个内部类,就变成了一个静态上下文
- 内部类不在依赖于外部类对象而存在
局部位置内部类
-
定义在类中的,方法体内
-
局部内部类,只能在定义内部类的方法体中创建该内部类对象
-
局部内部类的访问特征
-
局部内部类除了有所有内部类共有的访问特征之外,还有一个特征就是,
局部内部类可以访问方法体中的局部变量 -
但是局部内部类只能访问final的局部变量
-
原因
- 核心原因生命周期的问题
- 局部变量存储在栈帧中,栈帧随方法执行的结束,而被销毁
- 而局部内部类对象并不一定随着方法的执行而被销毁
- 即方法执行结束后,局部变量已经不存在,而匿名内部内对象,
还活着,因此它就可以去访问那个已经不存在的局部变量
-
匿名内部类对象
本质
是一个匿名(继承了类或者实现了接口的 匿名子类)对象
优点
不管是成员或局部位置内部类,我们要使用内部类都分成了2步:
-
定义内部类
-
创建内部类对象
通过定义匿名内部类对象,我们可以将上面的2步变为1步。
前提
存在一个类或者接口,这里的类可以是具体类也可以是抽象类。
语法
new 类名或者接口名() {重写方法;}
匿名内部类对象成员的访问
- 一个匿名内部类对象,因为是个匿名对象,所以每访问一次成员,都需要创建一个对象(除非用一个引用变量指向该对象)
使用场景
只使用某抽象类,或者接口子类对象一次的情况下使用
- 方法的形式参数,是引用类型的情况重点是接口 的情况
- 方法返回一个接口类型的子类对象的时候
Object api
地位
Object类是java中所有类,或直接或间接的父类
构造方法
只有无参构造方法
成员变量
无
成员方法
public final Class getClass()
作用
返回此 表示次对象的 的运行时类
Class Jdk定义的一个类,每一个 Class 类的对象,都代表了一个 运行时类
每一个类中
- 构造方法
- 成员变量
- 成员方法
等等把这些共性抽取出来 ,形成Class类
public String toString()
作用
- 返回该对象的字符串表示
- 通常,toString 方法会返回一个“以文本方式表示”此对象的字符串。
结果应是一个简明但易于读懂的信息表达式。建议所有子类都重写此方法。 - 对象的字符串表示: 将对象中的成员变量的值以字符串形式表示
默认实现
getClass().getName() + ‘@’ + Integer.toHexString(hashCode())
我们通常需要自己在子类中,重写该方法
public boolean equals(Object obj)
作用
指示其他某个对象是否与此对象“相等”。
对象相等的含义
- 同一个类的多个对象,他们的差别,仅仅在于对象中所存储的 成员变量的值
- 所以我们通常认为,同一个类的两个对象,成员变量的值相等
Object类的默认实现
- Object 类的 equals 方法实现 对象上差别 可能性最大 的相等关系;
- 对于任何非空引用值 x 和 y,当且仅当 x 和 y 引用同一个对象时,此方法才返回 true(x == y 具有值 true)
equals方法的常规协定(了解)
- 自反
- 对称
- 传递
- 一致性
public int hashCode()
作用
返回该对象的哈希码值
hash函数
A hash function is any function that can be used to map data of arbitrary size onto data of fixed size.
对象的hash码值
将一个对象映射到一个整数值,一个对象对应的整数值,即其hash码值
Object类的默认实现
- 由 Object 类定义的 hashCode 方法确实会针对不同的对象返回不同的整数
- 这一般是通过将该对象的内部地址转换成一个整数来实现的
常规协定
- 对象的hash码值,由比较两对象相等是所用的信息,计算得到
- 如果两对象通过equals方法比较相等,那么他们的hash码值应该相等
- 如果对象相等,则其hash码值必须相等,但是如果两对象不相等,不要求其hasn码值一定不同
protected Object clone()
作用
创建并返回此对象的一个副本
注意事项
-
但是Object类实现的clone方法,默认实现的额是浅拷贝
-
被复制的类必须实现Cloneable接口
-
clone方法的protected权限问题
- 有两个类A,B,都是Object的直接子类
- 在类A的类体中,可以直接访问类A继承的Object的clone方法
- 在类A类体中,可以通过创建A类对象,访问A类从Object中继承的clone方法
- 但是在类A类体中,却不能通过创建类B对象,访问类B继承自Object的clone方法
protected void finalize()
作用
当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法
String api
字符串是由多个字符组成的一串数据(字符序列)
构造方法
- public String()
- public String(byte[] bytes)
- public String(byte[] bytes,int offset,int length)
- String(char[] value)
- public String(char[] value,int offset,int count)
- public String(String original)
特征
- 字符串是"常量",它的值在创建之后不能更改
- 这里所谓的不可变,是指一个字符串对象一旦被创建,如果要修改字符串,
总是创建新的字符串对象,在新的字符串对象中,修改字符序列
常用api
判断功能
- boolean equals(Object obj)
- boolean equalsIgnoreCase(String str)
- boolean contains(String str)
- boolean startsWith(String str)
- boolean endsWith(String str)
- boolean isEmpty()
获取功能
- int length()
- char charAt(int index)
- int indexOf(int ch)
- int indexOf(String str)
- int indexOf(int ch,int fromIndex)
- int indexOf(String str,int fromIndex)
- String substring(int start)
- String substring(int start,int end)
转换功能
- byte[] getBytes()
- char[] toCharArray()
- static String valueOf(char[] chs)
- static String valueOf(int i)
- String toLowerCase()
- String toUpperCase()
- String concat(String str)
替换功能
- String replace(char old,char new)
- String replace(String old,String new)
去除字符串头,尾的空格
- String trim()
比较功能
- int compareTo(String str)
- int compareToIgnoreCase(String str)
Exception
基本概念
狭义的异常
- 简单来说异常就是用来表示Java程序运行过程中的错误(信息)
异常机制
- java语言程序,获取程序运行过程中的错误,并处理错误的机制
异常机制的由来
- C语言时代的错误处理,紧靠口头的约定,没有任何语法的强制约束
- Java的基本理念: 将错误尽可能的摒弃在jvm之外,所以在代码编译时做了很多额外的,代码检查工作
- 但是并非所有的错误都能在编译代码的时候处理,这意味着程序运行的时候,
也会发生一些错误,因此,java语言提供了一种统一的方式(机制),让程序自己在出错的时候,自己处理这些错误,
即一种程序运行时的错误恢复机制
异常机制的本质
- 提供了一种一致性的错误报告模型
- 使得类的构建者和使用者之间可以进行可靠的沟通
异常的分类(根据错误的严重程度)
Error
- 十分严重,已经不是软件层面可以解决的了,所以我们处理不了,也不关心
Exception’
-
严重程度,没有那么严重,我们自己或许可以处理
-
分类
-
编译时异常CheckableException
-
运行时异常RuntimeException
-
常见的异常:
空指针异常,数组越界异常
JVM的默认异常处理
当我们代码在执行到,发生错误的地方,jvm就会首先终止我们自己程序的运行,转而执行jvm自己的错误处理流程
在发生错误地方,收集错误信息,产生一个描述错误的对象
描述此次错误的异常对象,将错误信息,输出到控制台窗口中
JAVA语言提供的,java程序自己的异常处理
利用try-catch代码块
-
当我们写在,try代码块中的代码,出现异常的的时候,该异常的信息会被jvm收集起来,并交给我们自己的异常处理器,
-
每一个catch分支就是一个异常处理器
-
这个过程我们称之为异常的捕获
单分支的异常处理
- 如果try中代码,运行时,发生了错误,jvm在发生错误的代码处,收集错误信息
- try 块中在错误代码之后的代码,就不会在运行,jvm跳转到,相应的错误处理器中,执行有开发者,自己写的,错误处理代码
- 错误处理器中的代码,一旦执行完毕,紧接着程序继续向下正常执行,执行的是整个try代码块之后的代码
多分支的异常处理
原因
-
在一个try块中,可能出现多种类型的异常,如果只有一个catch分支(一个异常处理器),也就是说,所有类型的异常都会交给这个异常处理器来处理,但通常可能不同类型的异常,错误处理的方式不同,
-
因此,我们需要对同一个try块,针对可能发生的多种类型的错误,定义多种类型的异常处理器,即多种类型的catch分支,针对不同类型的异常,做不同的处理
多分支异常匹配过程
jvm 怎么知道,把异常对象交给哪个异常分支,并执行那个异常分支中的代码,这就存在一个多分支异常处理的匹配问题:
-
根据实际的异常对象的类型,和异常分支(异常处理器)声明的异常类型,从上到下一次做类型匹配
-
一旦通过类型匹配,发现实际异常对象的类型和Catch分支(异常处理器)声明的异常类型,类型匹配,就把异常对象交给这个异常分支(异常处理器)处理
-
多分支的异常处理的执行,有点类似于多分支if-else的执行,一次匹配,只会执行多个catch分支中的一个
注意事项
-
如果说,在多catch分支的情况下,如果不同的catch分支,处理的异常类型,有父子关系
那么就一定要注意,处理子类的异常分支写在前面,父类的异常分支写在后面 -
不是包裹在try块中的代码,一旦产生了异常,都是自己来处理,只有try中异常类型,
有对应类型的异常处理器的时候
获取异常信息
getMessage() 获取异常描述信息,返回字符串。
toString()获取异常类名和异常信息,返回字符串。
printStackTrace() 获取异常类名和异常信息,以及异常出现在程序中的位置,并打印到控制台
编译时异常VS运行时异常
运行时异常 无需显示处理,也可以和编译时异常一样处理
Java程序必须在写代码时显示处理,否则程序就会发生错误无法通过编译
空指针异常,数组越界异常,除0异常 它们其实都是运行时异常
异常的抛出
throws
作用
其语法约束,主要针对的是编译时异常:
-
在方法定义时使用
-
声明该方法可能抛出的异常(throws后面,跟一个异常列表)
-
对于编译时异常 ,可以在语法层面个强制方法调用者处理该异常
语法
修饰符 返回值(形参列表) throws 异常列表 {}
注意事项
注意事项1
- 对于运行时异常而言,jvm在其发生异常的时候,会自动,将异常信息,向上抛出(抛出给方法调用者)
- 但是,对于编译时异常而言,只有在方发声明中,用throws关键字声明过,可能抛出的编译时异常,
此时,这些被throws关键字声明的编译时异常,当实际发生的时候,才可以向上抛出
注意事项2
- 异常列表之间用逗号分割,列表中出现的异常不要出现包含关系
注意事项3
-
方法覆盖时的子类异常列表必须与父类兼容(有关方法覆盖)
-
如果父类方法中没有声明抛出编译时异常,
如果要覆盖父类方法,子类也不能有编译时异常的异常列表;
但是对于运行时异常,没有此要求 -
对于编译时异常,子类的异常列表可以和父类一模一样,也可以不一样,
但不一样的时候要保证,子类中方法的异常列表中的异常类型,
必须是父类异常列表中异常, 的子类类型; 但是对于运行时异常,没有此要求
-
throw
作用
- 在方法体中使用
- 主动在程序中抛出异常(真正的自己抛出动作,throw)
- 每次只能抛出确定的某个异常对象
执行特征
一旦执行了throw关键字的抛出异常对象语句,代码的执行,直接跳转到上一层(方法调用处)
基本语法
throw 异常对象
注意事项
若要抛出 编译时异常 , 则必须和throws配合起来使用
throws VS throw
- throws用在方法声明后面,跟的是异常类名 ;throw 用在方法体内,跟的是异常对象名
- throws 可以跟多个异常类名,用逗号隔开 ;throw一次只能抛出一个异常对象
- throws 表示抛出异常,由该方法的调用者来处理 ;表示抛出异常,可以由方法体内的语句处理
- throws throws表示出现异常的一种可能性,并不一定会发生这些异常
throw则是抛出了异常,执行throw则一定抛出了某种异常
异常处理的总结
我们所学习过的异常的处理策略主要有两种
- 捕获并处理
- 向上抛出
异常处理的原则
- 如果该功能内部可以将问题处理用try,-catch
- 如果处理不了,交由调用者处理用throws
异常一旦被捕获,并且没有再次被抛出,那么上层是感知不到该异常的!!!!
finally
执行特征
- 被finally控制的语句体一定会执行
- 特殊情况:在执行到finally之前jvm退出了(比如System.exit(0))
作用
用于释放资源,在IO流操作和数据库操作中会见到
自定义异常
很多情况下,jdk并没有提供现成的异常类,来描述我们自己特定应用场景下的异常情况,因此很多时候,我们需要自己定义异常来描述我们自己特定应用场景下的异常情况
实现
- 继承自Exception
- 继承自RuntimeException
File
概述
文件和目录 路径名的 抽象 表示形式
一个File类对象,它描述的就是一个,路径名字符串,所表示一个文件或目录
文件路径
-
绝对路径:绝对路径名是完整的路径名,不需要任何其他信息就可以定位它所表示的文件 f:\devlopment\a.txt
-
相对路径: 相对路径名必须使用取自其他路径名的信息进行解释 (相对对父路径) develop\b.txt
路径表示
类unix
- 对于 UNIX 平台,绝对路径名的前缀始终是 “/”。相对路径名没有前缀。
- 绝对路径: /dir/file.txt /表示根目录
- 相对路径: dir/a.exe
windows
- 绝对路径有前缀 盘符:\,相对路劲没有盘符:\前缀的路径
默认情况下,java.io 包中的类总是根据当前用户目录来解析相对路径名。此目录由系统属性 user.dir 指定
相对路径: development\b.txt = 当前用户路径 + develop\b.txt
api
创建
-
public boolean createNewFile() //在操作系统上物理创建文件
-
public boolean mkdir() //创建目录
-
public boolean mkdirs()//创建目录
-
makir和mkdirs的区别
- 当目标目录的父目录不存在的时候,mkdir()会创建目标目录失败
- 当目标目录的父目录不存在的时候,mkdirs(),会连同目标目录和不存在的父目录,一起创建出来
删除
- public boolean delete() 删除文件或目录, 如果此路径名表示一个目录,则该目录必须为空才能删除
重命名
- public boolean renameTo(File dest)
- 如果是目标文件对象和当前文件对象,在同一目录下,实现的效果就是重命名
- 如果是目标文件对象和当前文件对象,不在同一目录下,除了重命名,还有文件移动效果
判断功能
- public boolean isFile() 判断当前File对象是否表示文件
- public boolean isDirectory() 判断当前File对象是否表示目录
- public boolean exists() //物理判断File对象表示的文件或目录是否真的存在
- public boolean canRead() 判断文件是否可读
- public boolean canWrite() 判断文件是否可写
- public boolean isHidden() 判断文件是否隐藏文件
获取功能
-
public File getAbsoluteFile() 获取文件的绝对路径字符串
-
public String getPath() 获取文件的路径字符串
-
public String getName() 获取文件的文件名
-
public long length(),返回由此抽象路径名表示的文件的长度。如果此路径名表示一个目录,则返回值是不确定的。大小单位为字节
-
public long lastModified() 表示文件最后一次被修改的时间的 long 值,用与时间点(1970 年 1 月 1 日,00:00:00 GMT)之间的毫秒数表示
-
高级获取功能
-
public String[] list()
- 返回一个字符串数组,这些字符串指定此抽象路径名表示的目录中的文件和目录。
- 如果此抽象路径名不表示一个目录,那么此方法将返回 null
-
public File[] listFiles()
- 返回一个抽象路径名数组,这些路径名表示此抽象路径名表示的目录中的文件
- 如果此抽象路径名不表示一个目录,那么此方法将返回 null
-
-
自定义获取功能
-
自定义在哪里呢? 就是我们可以自己指定,所要查找的文件,需要满足的过滤条件
list或listFiles方法的参数,它们都是用来表示,你定义的过滤条件的 -
File[] listFiles(FileFilter filter)
-
String[] list(FilenameFilter filter)
-
File[] listFiles(FilenameFilter filter)
-
两种表示过滤条件的接口
- public interface FilenameFilter
- public interface FileFilter
-
IO流
IO 产生的原因
在操作系统中,一切数据都以文件的形式存储。
需要长久保存的文件数据,存储在外部设备。
程序运行时,所有的数据都需要在内存中
同时,内存的大小有限,因此常常需要在内存和外设之间交换数据,即I/O
而Java语言主要通过输入流和输出流,完成I/O的功能,从而实现和外设的数据交互
IO流
IO流用来处理JVM和外部设备之间的数据传输Java通过流(Stream)的方式,完成数据的传输过程。
IO流的分类
数据流动的方向
输入流
- 对应数据的读入,将外设数据读入内存
输出流
- 对应数据的写出,将数据写入到外设
流中的内容
-
字节流
-
流中的数据,是以字节为单位的二进制数据
-
字符流,是以字符为单位的字符数据
-
区分使用字节流还是字符流
-
字符流
- 文本编辑器可以打开,并且人可以看懂的数据
-
字节流
- 字节流可以操作一切类型的数据
只是有时操作字符数据不太方便,所以字符数据专门交给字符流
- 字节流可以操作一切类型的数据
-
-
IO流的基类
字节流
-
输入流
- InputStream(抽象类)
-
输出流
- OutputStream(抽象类)
字符流
-
输入流
- Reader(抽象类)
-
输出流
- Writer(抽象类)
由这四个类派生出来的子类名称都是以其父类名作为子类名的后缀
如:InputStream的子类FileInputStream。
如:Reader的子类FileReader
字节流
输出流
OuputStream
实例化
-
OutputStream是抽象类,无法直接实例化,只能间接实例化,
因为,通常操作的都是文件数据,所以使用其操作文件的具体子类FileOutputStream实例化 -
FileOutputStream构造方法
- FileOutputStream(File file)
- FileOutputStream(String name)
使用,即向外设写入数据
- public void write(int b)
- public void write(byte[] b)
- public void write(byte[] b,int off,int len
字节流写数据常见问题
-
创建字节输出流到底做了哪些事情?
-
- FileOutputStream会先在操作系统中,找目标文件
a. 如果说目标文件不存在,FileOutputStream创建这个文件
b. 如果改文件存在,则不再重新创建,清空文件内容,准备从文件最开始的地方写入
- FileOutputStream会先在操作系统中,找目标文件
-
- 在内存中,创建FileOutputStream对象
-
- 在FileOutputStream和目标文件之间建立数据传输通道
-
-
数据写成功后,为什么要close()?
- 关闭此输出流并释放与此流有关的所有系统资源。
-
如何实现数据的换行? 利用换行符
- windows操作系统: ‘\r’’\n’
- linux,Mac OS: ‘\n’
- 对于一些高级记事本,都可以识别
-
如何实现数据的追加写入?
- FileOutputStream(File file, boolean append)
- 创建一个向指定 File 对象表示的文件中写入数据的文件输出流。如果第二个参数为 true,
则将字节写入文件末尾处,而不是写入文件开始处
-
给I/0流操作加上异常处理
输入流
InputStream
实例化
- InputStream是抽象类,无法直接实例化,只能间接实例化,
因为,通常操作的都是文件数据,所以使用其操作文件的具体子类FileInputStream实例化
FileInputStream构造方法
- FileInputStream(File file)
- FileInputStream(String name)
使用,即从外设读入数据
- public int read()
- public int read(byte[] b)
一次读入读入或写出一个字节效率高,还是一次读入或写出一个字节数组效率高
-
一个字节数组效率高
-
原因
- 每次,读入或写出,
即每次和外设的数据交互都需要依赖操作系统内核实现 - 这意味着每次读入或写出,都需要付出额外的通信代价
- 一次读入或写出一个字节数组的数据,
平均到每个字节,付出的额外代价少很多
- 每次,读入或写出,
缓冲流
产生原因
- 字节流一次读写一个数组的速度明显比一次读写一个字节的速度快很多
- 这是加入了数组这样的缓冲区效果
- java本身在设计的时候,也考虑到了这样的情况,所以提供了字节缓冲区流
字节缓冲输出流BufferedOutputStream
字节缓冲输入流BufferedInputStream
字符流
字符流产生的原因
在某些情况下,用字节流来操作中文不太方便。
之所以不方便,是因为,有些字符,需要多个字节数据来表示,而字节流数据的单位是单字节
因此字节流中,就可能出现不完整的字符表示
即核心原因在于:数据逻辑单位不一致
字符的表示
字符在计算机中,是以二进制数据的形式存储,即字符在计算机中对应的是一个二进制的整数值
字符集(编码表)
-
由字符及其对应的数值组成的一张表
-
即字符所对应的二进制数值,是由字符(编码表)集规定的
-
常见字符集
-
ASCII:美国标准信息交换码。
用一个字节的7位可以表示。 -
ISO8859-1:拉丁码表。欧洲码表
用一个字节的8位表示。 -
GB2312:中国的中文编码表。
-
GBK:中国的中文编码表升级,融合了更多的中文文字符号。
-
GB18030:GBK的取代版本
-
BIG-5码 :通行于台湾、香港地区的一个繁体字编码方案,俗称“大五码”。
-
Unicode:国际标准码,融合了多种文字。
-
UTF-16
- 定长编码两个字节表示一个字符
-
UTF-8
-
UTF-8:可变长度来表示一个字符。
-
UTF-8不同,它定义了一种“区间规则”,
这种规则可以和ASCII编码保持最大程度的兼容:- 它将Unicode编码为00000000-0000007F的字符,用单个字节来表示
- 它将Unicode编码为00000080-000007FF的字符用两个字节表示
- 它将Unicode编码为00000800-0000FFFF的字符用3字节表示
- 1字节 0xxxxxxx
- 2字节 110xxxxx 10xxxxxx
- 3字节 1110xxxx 10xxxxxx 10xxxxxx
-
-
-
编解码
-
编码:将字符转化为对应的二进制整数值(基于指定字符集)
-
解码:将整数值转化为对应的字符(基于指定字符集)
-
编解码使用的一定是同一个字符集(编码表),否则可能出现乱码问题
-
对应Java语言的编解码操作
-
编码
- 字符串对象.getBytes(“指定字符集或者编码表的名称”)
- 当没有指定所用编码表或字符集的时候,在IDEA中默认使用UTF-8,如果没有使用IDEA原生的默认字符集是GBK
-
解码
- String(byte[], String chasetName)
- String(byte[], int offset, int len, String charsetName)
- 当没有指定所用编码表或字符集的时候,在IDEA中默认使用UTF-8,如果没有使用IDEA原生的默认字符集是GBK
-
-
乱码问题产生的核心原因:编解码所使用的字符集不一致
字符流的实质
字符流 = 字节流 + 编码表(根据指定编码表,编解码的过程)
字符的存储和传输天然与二进制数据密切相关
字符流需要在二进制的基础上,添加基于特定编码表的字符编解码
字符输入流:是解码+字节流
字符输出流:编码+字节流
字符流体系
字符流的基类
-
Writer
-
具体子类
-
OutputStreamWriter
- public OutputStreamWriter(OutputStream out)
创建使用 默认字符编码(对于IDEA而言UTF-8, 但是原生情况下默认字符集GBk)的 OutputStreamWriter。 - public OutputStreamWriter(OutputStream out,String charsetName)
创建使用给定字符集的 OutputStreamWriter。
- public OutputStreamWriter(OutputStream out)
-
FileWriter
- 用来写入字符文件的便捷类
- public FileWriter(String fileName),
创建使用默认字符集的,字符输出流,专门向文件中写入字符数据 - public FileWriter(Flie file)
- public FileWriter(String fileName, boolean append)
该构造方法用来实现简化流的文件的追加写入
-
BufferedWriter
- BufferedWriter(Writer out)
创建一个使用默认大小输出缓冲区的缓冲字符输出流。 - BufferedWriter(Writer out, int sz)
创建一个使用给定大小输出缓冲区的新缓冲字符输出流 - 特有的方法:void newLine() 向流中写入换行符
- BufferedWriter(Writer out)
-
-
write方法
- void write(char[] cbuf) 写入字符数组。
- void write(char[] cbuf, int off, int len) 写入字符数组的某一部分。
- void write(int c) 写入单个字符。 要写入的字符包含在给定整数值的 16 个低位中,16 高位被忽略
- void write(String str) 写入字符串。
- void write(String str, int off, int len)写入字符串的某一部分。
-
-
Reader
-
具体子类
-
InputStreamReader
- public InputStreamReader(InputStream in)
创建一个使用 默认字符集 的 InputStreamReader。 - public InputStreamReader(InputStream in,String charsetName)
创建使用指定字符集的 InputStreamReader。
- public InputStreamReader(InputStream in)
-
FileReader
- 用来读取字符文件的便捷类
- public FileReader(String fileName)
创建使用默认字符集的字符输入流,从指定文件中读取字符数据 - public FileReader(File file)
-
BufferedReader
- BufferedReader(Reader in)
创建一个使用默认大小输入缓冲区的缓冲字符输入流。 - BufferedReader(Reader in, int sz)
创建一个使用指定大小输入缓冲区的缓冲字符输入流。 - 特有方法: String readLine() 读取一行文本数据(不包括该行的换行符)
- BufferedReader(Reader in)
-
-
read方法
- int read()
读取单个字符。如果已到达流的末尾,则返回 -1 - int read(char[] cbuf)
将字符读入数组。读取的字符数,如果已到达流的末尾,则返回 -1 - int read(char[] cbuf, int off, int len)
将字符读入数组的某一部分。
- int read()
-
字符流是不可以用来传输图片,视频数据
- a. 图片和视频 图片文件数据和视频文件数据,都有自己的编码方式,而这种编码方式和
字符编码,没有任何关系 - b. 所以,在图片和视频的编码数据中,就会存在一些,字符集中不存在的编码值
此时在读入,视频和图片数据的时候,就会遇到一些在字符集中,找不到编码值 - c. 于是在转化的时候,如果遇到不认识的编码值,要么直接丢弃,
要么就用一个固定的???来表示未知编码值所对应的字符 - 这也就意味着, 视频和图片数据,已经被改变了
其他流
-
标准输入输出流
- 标准输入流
- 标准输出流
-
DataInputStream & DataOutputStream
-
打印流
-
序列化流
多线程
直观效果
多线程可以使得我们程序中不同的功能“同时”运行起来
多线程的理解
一个线程相当于程序的一条独立的执行路径
同一个线程中的代码,按照顺序依次执行(我们之前的代码,其实都是运行在一个线程中的,所以所有代码依次执行)
不同线程,即不同执行路径中的代码,可以“同时”运行
多线程产生的原因
操作系统的角度
提高cpu的利用率,减少切换的代价
-
单道批处理系统
- 内存中只有一个程序运行,cpu利用率不高
-
多道批处理系统
- 多个程序,交替运行,提高了cpu利用率,但应用程序间的切换,付出了较高的代价
-
现代操作系统中引入线程
- 使得一个进程中,可以有多条执行路径,程序可以在同一个进程的多条,线程间切换,减小切换代价
程序的角度
可以让,我们的程序中的不同功能,"同时"运行起来,提高我们自己程序的运行效率
java程序的运行
java命令运行java程序的过程
- 其实java命令,它启动了一个jvm进程
- 该jvm进程,在执行的时候,首先会创建一个线程,main线程
- 在main线程中,运行主类中的main方法代码
jvm是单线程还是多线程?
- 多线程
- 因为在java程序运行时,至少还有一个线程在做垃圾回收的工作
多线程的第一种实现方式
实现步骤
- 定义类继承Thread
- 重写子类的run方法
- 创建该子类的对象
- 启动线程 start()
注意事项
- 一个Thread类(Thread子类)对象代表一个线程
- 只有Thread run()方法中的代码,才会执行在子线程中
- 但是,如果想要让代码,在子线程中运行,并非一定,代码要写在run方法方法体中
对于,定义在该Thread子类中,其他方法方法体中的代码,也可以运行在子线程
换句话说,一个方法,被哪个线程中的代码调用,被调用的方法,就运行在,调用它的线程中 - 启动线程,必须使用start()方法来启动,这样才能是Thread中的run方法运行在子线程中
如果, 如果通过调用run方法,来执行Thread的run方法代码,这仅仅只是普通的方法调用 - 同一个Thread或Thread子类对象(代表同一个线程),只能被启动一次
如果,我们要启动多个线程,只能创建多个线程对象,并启动这些线程对象
线程api
线程信息api
-
线程名
- public final String getName()
public final void setName(String name)
- public final String getName()
-
static Thread currentThread()
返回对 当前正在执行的线程 对象的引用。 -
优先级
-
public final int getPriority()
public final void setPriority(int priority) -
java语言中的线程调度方式——抢占式调度
-
注意事项
- java语言中的Thread的priority,它其实没有什么太大用处 (统计意义,总的执行概率)。
- 因为线程调度,归根结底主要是操作系统内核,去完成的,jvm起不了决定性作用
操作系统中,有自己的一套优先级规则(动态优先级),基本是上我们设置的静态优先级,意义不是很大 - 优先级的取值范围1-10,默认的线程优先级是5
-
线程控制api
-
public static native void sleep(long millis)
- 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行), 暂停当前线程的执行
当前线程,指的是sleep,的调用线程
- 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行), 暂停当前线程的执行
-
public final void join()
- 等待该线程终止。
- 谁等待谁? 当前线程等待(调用join方法的线程),等待谁呢?等待该线程(调用join 的那个线程对象所代表的线程)
-
public static native void yield()
- 暂时让当前线程放弃cpu的执行权
-
public final void setDaemon(boolean on)
- 将该线程标记为守护线程或用户线程。
- 当正在运行的线程都是守护线程时,Java 虚拟机退出(jvm终止执行)。
该方法必须在启动线程前调用。
-
public void interrupt()
- 如果线程在调用 Object 类的 wait()、wait(long) 或 wait(long, int) 方法,
或者该类的 join()、join(long)、join(long, int)、sleep(long) 或 sleep(long, int) 方法过程中受阻,
它还将收到一个 InterruptedException。
- 如果线程在调用 Object 类的 wait()、wait(long) 或 wait(long, int) 方法,
线程生命周期
新建
线程处于刚刚创建的状态
就绪
有执行资格,等待cpu调度获得执行权
运行
取得执行权,正在cpu上执行
阻塞
无执行资格,无执行权
死亡
线程正常或异常终止(run()方法执行完毕),线程对象成为垃圾,等待垃圾回收器回收
线程实现方式2
实现步骤
- 实现Runnable接口的run方法
- 创建该子类对象
- 在创建Thread对象的时候,将创建好的Runnable子类对象作为初始化参数,传递给Thread对象
- 启动Thread对象(启动线程)
比较
-
- 从实现的步骤上来说,第二种实现方式要麻烦一点
-
- 从逻辑上来说,虽然麻烦了一点,但是我们认为第二种实现方式更好(方式二将线程和任务解耦)
-
- 方式一的实现方式,存在单重继承的局限性
-
- 方式二,便于多线程数据的共享 (基于threadsafe包下的edition1 和 edition2代码的比较)
多线程数据安全问题
问题
- 在多线程环境下,当多个线程,同时访问被多线程共享的数据时,可能会得到不正确的结果
问题原因
- 多线程运行环境
- 数据共享,即多线程共享数据
- 共享数据的非原子操作
解决
-
利用同步代码块,进行线程同步,从而在多线程环境下构造对共享变量的原子操作
-
同步代码块
-
语法
- synchronized(锁对象) {
需要同步的代码块
}
- synchronized(锁对象) {
-
加锁解锁过程
- 锁对象,锁对象就可以表示一把锁,java语言汇总所有的对象都可以作为锁对象
所有的对象中,都有一个标志位,这个标志位就是用来表示,加锁和解锁两种锁的状态 - 执行synchronized代码块前,jvm会尝试在当前线程中设置锁对象的标志位,
从而对锁对象加锁 - 成功获取锁的线程,在执行完synchronized代码块后,jvm会在该线程中,重置锁标志位,
即使当前线程释放锁
- 锁对象,锁对象就可以表示一把锁,java语言汇总所有的对象都可以作为锁对象
-
注意事项
- 锁对象可以是任意对象,但是注意,多线程对同一个共享变量的访问,必须使用同一把锁
- 写在同步代码块中的代码中代码,通常是多线程环境下多共享变量的一组操作,
将他们放在同步代码块中,就变成了一组原子操作
-
-
同步代码块相关概念
-
线程同步
- 我走你不走
-
线程异步
- 你走你的,我走我的
-
同步代码块其实是通过线程同步,构造原子操作,解决了线程安全问题
-
同步的好处
- 解决了多线程线程安全问题。
-
同步的缺点
- 相比于异步,因为等待锁资源而引发的阻塞,降低了程序运行效率。
-
-
同步代码块
- 被synchronized关键字修饰的方法是同步方法
- 同步方法的锁对象是当前对象this
Lock锁对象
实现同步代码块,除了使用synchronized之外,其实JDK1.5之后,提供了另外的方式Lock锁机制
Lock对象 VS 同步代码块中的锁对象
- 任意对象只有和synchronized配合才能成为锁对象,而Lock对象就是一种专门的锁对象
- 对同步代码块中的锁对象,其加锁解锁都是由jvm完成,但是对于Lock对象,其加锁解锁都是由Lock类中定义的方法完成
语法
- 表示锁的接口,其实现机制与synchronized不同
- lock() 加锁
- unlock() 解锁
注意事项
对于线程同步,可以使用synchronized代码块完成,也可以使用Lock对象完成,推荐使用synchronized
死锁
同步另一个弊端:如果出现了嵌套锁,可能产生死锁
死锁是指两个以上的线程在执行过程中,因为争夺资源而产生的一种相互等待的现象
如何解决死锁问题?
- 调整线程获取锁的顺序,让所有的线程,获取多把锁的顺序相同
- 让获取多把的线程,一次获取到所需的多把锁,然后执行
也就是说,我们现在需要把,获取多把锁,原子操作
生产者消费者问题
问题描述
-
有多个生产者和多个消费者,和一个容量为n的缓冲区
生产者负责生产产品放入缓冲区,消费者负责从缓冲区中取产品出来消费 -
多个生产者和多个消费者各自都是以异步的方式运行
-
约束条件
- 当缓冲区空的时候,不允许消费者到缓冲区中取数据
- 当缓冲区满的时候,不允许生产者向缓冲区中放入数据
- 同时缓冲区中的一个单元,只能放入一个产品
我们实现的是一个简化版的,缓冲区容量为1的生产者消费者问题,即做包子和卖包子问题
实现上述简化版的生产者,消费者问题,最关键在于两个动作——阻止自己,通知别人
方法实现
上述的两个关键动作,主要有Object中的方法实现
阻止自己
wait()
-
作用
- 导致当前线程等待 在哪个线程中调用wait()方法,当前线程就会处于阻塞状态,
- 在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,
才可以唤醒被wait()阻塞的线程
-
注意事项
-
调用wait()的条件
- 当前线程必须拥有此对象监视器(锁对象 Monitor)
即当前线程必须该对象的锁
- 当前线程必须拥有此对象监视器(锁对象 Monitor)
-
调用时发生什么
- 该线程发布对此监视器的所有权并等待, 当前线程首先放弃对锁对象的持有,
阻塞自己,让自己处于等待状态
- 该线程发布对此监视器的所有权并等待, 当前线程首先放弃对锁对象的持有,
-
唤醒条件
- 直到其他线程通过调用 notify 方法,
或 notifyAll 方法 通知 在此对象的监视器上(锁对象) 等待的线程 醒来 - 在一个锁对象上阻塞的线程,只能在同一个锁对象杀调用 notify或 notifyAll
- 直到其他线程通过调用 notify 方法,
-
sleep VS wait
-
所属不同
- sleep定义在Thread类,静态方法
- wait定义在 Object类中,非静态方法
-
唤醒条件不同
- Sleep方法是休眠时间到
- 在其他线程中,在同一个锁对象上,调用了notify或notifyAll方法
-
使用条件不同:
- Sleep 没有任何前提条件
- wait(), 必须当前线程,持有锁对象,锁对象上调用wait()
-
休眠时,对锁对象的持有情况不同
- 线程因为Sleep方法而处于阻塞状态的时候,
在阻塞的时候不会放弃对锁的持有 - 但是wait()方法,会在阻塞的时候,放弃锁对象持有
- 线程因为Sleep方法而处于阻塞状态的时候,
通知别人
notify()
- 唤醒在此对象监视器上等待的 单个线程, 如果所有线程都在此对象上等待,
则会选择唤醒其中一个线程。选择是任意性的 - 直到当前线程放弃此对象上的锁定,才能继续执行被唤醒的线程 直到锁对象被释放之后,
才会真正的去唤醒其他线程
notifyAll()
- 唤醒在此对象监视器上等待的所有线程
- 直到当前线程放弃此对象上的锁定,才能继续执行被唤醒的线程
完整的线程的状态转化
完整版的状态转化图,将阻塞状态细分为了3种
阻塞状态
-
其他阻塞
- 因sleep,join,io等原因,处于运行态的线程可能会进入阻塞状态
- 处于其他阻塞状态的,线程,当执行条件满足时,又可以进入就绪态
-
同步阻塞
- 运行态的线程,因为线程同步,而处于阻塞状态的线程,会进入同步阻塞状态
- 或者处于等待阻塞状态的线程,被唤醒后也会处于同步阻塞状态
- 处于同步阻塞状态的线程,只有当争抢到锁的持有权,才能进入就绪态
-
等待阻塞
- 运行态的线程,因为调用了wait方法而处于等待阻塞状态
- 处于等待阻塞状态的线程,当被别的线程通过notify或者notifyAll方法唤醒的时候,
会进入同步阻塞状态
网络编程
实质
实现两台主机之间的,进程间通信
即Java程序,利用计算机网络中的传输层提供的功能,实现底层数据传输,并基于该底层的数据传输功能,实现Java应用程序的功能
3要素
IP
- 计算机在网络中的“地址”,或者说唯一标识
- 一个InetAddress类对象,表示一个ip地址
- 通过InetAddress.getByName方法,得到InetAddress对象
端口号
- 进程在某计算机中的逻辑地址,或者唯一标识
- ip + 端口号才能唯一确定要通信的目标地进程
- 取值0-65535,当时我们可以用的端口号只能是1024-65535
传输层协议
-
Java类库中,定义了各种Socket类(类名都带Socket),来抽象传输层的功能,供我们的应用程序使用。
对于TCP和UDP分别定义类不同类型的Socket类,来实现分别基于TCP和UDP协议的输出传输。 -
TCP
-
发送端(客户端)
- Socket
-
接收端(服务器端)
- ServerSocket
-
-
UDP
-
发送端/接收端
- DatagramSocket
-
UDP编程
发送端(客户端)
-
- 创建用于发送数据的socket对象
- DatagramSocket(int port)
- 创建数据报套接字 并将其绑定到 本地主机上的指定端口。
-
- 将要发送的数据封装成数据包
- DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port)
- 构造数据报包,用来将长度为 length 偏移量为
offset 的包(其实是字节数组数据)发送到指定主机上的指定端口号。
-
- 利用udp的socket对象,将数据包发送出
- send(DatagramPacket p)
-
- 释放资源
- close()
接收端(服务器端)
-
- 建立udp的socket对象.
- DatagramSocket(int port)
- 创建数据报套接字 并将其绑定到 本地主机上的指定端口。
-
2.创建用于接收数据的数据报包,
通过socket对象的receive方法接收数据- DatagramPacket(byte[] buf, int offset, int length)
构造 DatagramPacket,用来接收长度为 length 的包,在缓冲区中指定了偏移量。 - receive(DatagramPacket p)
该方法是一个阻塞方法,当没有数据时,该方法会使当前线程阻塞
- DatagramPacket(byte[] buf, int offset, int length)
-
- 解析接收到的数据报包中的数据
- //从接收到的数据报包中,获取接收到的数据
byte[] data = packet.getData(); - //接收数据的时候,数据是从字节数组缓冲区的哪个位置开始填充的
int offset = packet.getOffset(); - //接收数据的时候,本次接收到的数据的字节个数
int length = packet.getLength();
-
- 释放资源
- close
注意事项
- 接收端即使没有启动,发送端随时可以发送数据,而不报错
所以说UDP协议,无连接不可靠的协议,不管对端有没有接收到,客户端只管发送数据 - 同一台主机上的同一个端口号,同时只能被一个进程使用
如果想要将同一个端口号分配给两个进程,则会抛出异常java.net.BindException: Address already in use: Cannot bind
TCP编程
概述
- 基于TCP的socket使用起来,似乎要简单许多!因为基于TCP的Socket的数据传输是基于流来实现的。
发送端(客户端)
-
建立客户端的socket对象,并明确要连接的服务器
- Socket(String host, int port)
创建一个流套接字并将其连接到指定主机上的指定端口号。 - host,要和发送端建立连接的,接收端的IP地址
- port, 要和发送端建立连接的,接收端的端口号
- Socket(String host, int port)
-
如果对象建立成功,就表明已经建立了数据传输的通道.
就可以在该通道通过IO进行数据的读取和写入 -
根据需要从socket对象中获取输入,或输出流
- OutputStream getOutputStream()
在Socket对象上获取输出流,用于发送数据 - InputStream getInputStream()
在Socket对象上获取输入流, 用于接收数据
- OutputStream getOutputStream()
-
向流中读取或写入数据
-
释放资源
服务器(服务器端)
-
创建Serversocket对象,在指定端口,监听客户端连接请求
- ServerSocket(int port)
创建一个在本机上,监听指定端口的ServerSocket
该对象的,的功能主要是,接收客户端的连接请求,并建立连接
- ServerSocket(int port)
-
收到客户端连接请求后,建立Socket连接
-
Socket accept()
- 侦听并接受到此套接字的连接。此方法在连接传入之前一直阻塞。
- accept方法在接收到客户端连接请求后,会创建一个Socket对象,
该Socket对象和客户端Ssocket对象建立连接
-
-
从Socket对象中,根据需要获取输入,或输出流
- OutputStream getOutputStream()
在Socket对象上获取输出流,用于发送数据 - InputStream getInputStream()
在Socket对象上获取输入流, 用于接收数据
- OutputStream getOutputStream()
-
根据需要向流中写入数据或从流中读数据
-
释放资源
- close
注意事项
-
因为TCP协议中,当客户端要发送数据时,必须首先建立连接,
所以当没有启动接收端(服务器端),而直接发送数据,会报错——ConnectException -
同UDP, 一个端口号,只能分配给一个进程,否则,会产生java.net.BindException: Address already in use: Cannot bind
-
Socket对象中的InputStream的read方法是阻塞方法
-
解决客户端等待服务器端响应,而产生的相互等待的问题
-
- 可以通过自定义结束标志解决
- 2.利用Socket对象的socket.shutdownOutput()方法解决
-
-
基于TCP的通信中,利用多线程,改善多个客户端连接服务器端的效率
反射
类加载过程
步骤
-
加载
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
-
连接
- 通过一系列的检查,确保被加载类的正确性
- 负责为类的静态成员分配内存并设置默认初始化值
- 将类中的符号引用替换为直接引用
-
初始化
- 给静态成员变量赋初值,执行静态代码块内容
类加载时机(何时加载)
- 创建类的实例
- 访问类的静态变量
- 调用类的静态方法
- 使用反射方式来强制创建某个类或接口对应的java.lang.Class对象
- 初始化某个类的子类,会先触发父类的加载
- 直接使用java.exe命令来运行某个主类
类加载器(谁加载)
-
Bootstrap ClassLoader 根类加载器
- 负责Java核心类的加载,JDK中JRE的lib目录下rt.jar
-
Extension ClassLoader 扩展类加载器
- 负责JRE的扩展目录中jar包的加载,在JDK中JRE的lib目录下ext目录
-
Sysetm ClassLoader 系统类加载器
- 负责加载自己定义的Java类
反射技术
反射技术的实质是,帮助我们获取运行时类型信息,即在程序运行过程中,动态获取数据类型相关信息
在程序运行过程中,从哪里获取,运行时类型信息?
- Class对象,即Class对象的存在, 是反射技术的起点
- 说白了,反射技术就是从通过访问Class对象,获取运行时类型信息的
如何获取Class对象
-
通过对象
- Object 类的getClass()
-
通过字面值常量
- 类名.class
-
通过静态方法
- Class.forName(String 类名)
构造方法
-
Constructor类是用来描述所有的构造方法的,一个Consturctor对象就代表一个构造方法
-
获得构造方法
-
获得类中定义的多个构造方法
-
Constructor[] getConstructors()
- 获取类中定义的所有public的构造方法信息
-
Constructor[] getDeclaredConstructors()
- 获得类中定义的所有构造方法信息
-
-
获得类中定义的指定构造方法
-
Constructor getConstructor(Class… parameterTypes)
- 获得指定的public的构造方法
-
Constructor getDeclaredConstructor(Class… parameterTypes)
- 获得指定的构造方法
-
-
-
使用构造方法(Constructor对象所代表的构造方法)
-
newInstance(参数列表)
- 创建该类型对象
-
-
暴力破解权限问题
- void setAccessible(boolean flag)
成员变量
-
Field类,用来描述类中定义的成员变量,一个Field对象,表示一个类中定义的一个成员变量
-
获得成员变量
-
获得多个成员变量
-
Field[] getFields()
- 可以获取当前类中所定义的所有的公有成员变量,
以及当前类的所有父类中所定义的公共成员变量
- 可以获取当前类中所定义的所有的公有成员变量,
-
Field[] getDeclaredFields()
- 可以获取 当前类 中定义的所有成员,
但是该方法无法获取到父类中定义的成员变量
- 可以获取 当前类 中定义的所有成员,
-
-
获得单个指定成员变量
-
Field getField(String name)
- 获取当前类以或其父类中的公共方法
-
Field getDeclaredField(String name)
- 在当前类中获取指定名字的方法
-
-
-
访问成员变量
-
Object get(Object obj)
- 在某对象上获取,其成员变量的值
-
void set(Object obj, Object value)
- 在某对象上,设置成员变量的值
-
-
暴力破解权限问题
- void setAccessible(boolean flag)
成员方法
-
Method类,用来描述类中定义的方法,一个Method对象,表示类中定义的一个方法
-
获得成员方法
-
获得多个成员方法
-
Method[] getMethods()
- 获取当前类以及父类中的公共方法
-
Method[] getDeclaredMethods()
- 当前类中,所定义的所有方法
-
-
获得单个指定成员方法
-
Method getMethod(String name, Class… parameterTypes)
- 获取当前类或其祖先类中定义的公共方法
- String name 是指我们要获取的方法的方法的名称
- Class… parameterTypes 指方法的参数列表
- 注意事项: 如果该类及其直接或间接父类中,未定义该方法,则会抛出异常
-
Method getDeclaredMethod(String name, Class… parameterTypes)
- 获取当前类中定义的指定方法
-
-
-
调用成员方法
-
Object invoke(Object obj, Object… args)
- 在某对象上,调用该方法(并传递实际参数值)
- obj 要调用方法的那个对象
- Object… 方法调用是,传递的实际参数
- Method对象.invoke(obj, ars)
-
-
暴力破解权限问题
- void setAccessible(boolean flag)
注解
在代码中添加额外信息
注释
- 注释中的描述,是一种人为的约定
- javac在编译时,对其“视而不见”
- 只有固定语法,没有标准形式
- 因此,注释中表达的描述代码或者代码之外的信息,只有人能看懂。
注解
- 为我们在代码中添加信息,提供了一种形式化的方法
- 使我们可以在稍后的某个时刻非常方便的使用这些信息
常见注解
@Override
- 声明某方法,覆盖了父类方法
@Deprecated
- 声明方法或变量已过时
自定义注解
原因
- Java语言中本身只定义了极少数的注解
- 当然不能满足我们所有的需求,但是Java语言,可以让我们自定义注解,满足自己的需求。
语法
-
定义注解格式
-
public @interface 注解名 {
定义体
} -
注意事项
- 我们所定义的一个注解,其实就是一种数据类型
- 注解的定义体,就定义了该种类型的额外信息(注解),包含多少条具体信息,每种具体信息的名称及数据类型
- 从形式上看,十分类似于接口定义
- @必不可少
- 注解之间不能继承
-
-
注解体格式说明
-
注解体中定义的,就是一条一条的具体额外信息,每条额外信息的名称,以及取值的数据类型
-
方法名就是数据的名称,方法的返回值类型,表示数据值的类型。
-
格式
- int maxAge();
-
注解中每个额外信息的值,即属性值的合法数据类型
- 所有的基本类型
- String
- Class
- Annotation
- 以上类型的数组
-
-
注解的使用
-
即使用,定义好的注解类型的,实例,给代码添加额外信息
-
格式
- @注解的类型名(属性名1=属性值1, 属性名2=属性值2…)
-
注意事项
-
在使用每个具体注解实例的时候,必须保证注解实例中的每个数据不能有不确定的值!!
-
注解实例中,属性赋值有两种方式
-
在定义注解时,给注解中的属性赋予默认值
- 在定义注解时,给每个注解定义默认值,
这样的话在使用注解时,没被赋值的数据就用默认值 - 但是注意引用类型的数据,其默认值不能取null!!!
- 在定义注解时,给每个注解定义默认值,
-
在使用注解时,给属性赋值
- 在使用注解的时候,如果注解的名字是value,
且只有名为value的数据需要赋值,这样的赋值可以简写
- 在使用注解的时候,如果注解的名字是value,
-
-
-
注解的处理
- 必须使用注解处理器,来处理获取和处理注解信息
- 注解处理器本身,没有什么特殊的语法,只是通过一些其他方式(比如反射技术),获得所需注解信息
然后根据需求实现特殊功能
元注解
概念
- 定义注解时所使用的注解
- 即注解的注解
常用元注解
-
@Retention
-
定义注解的保留级别
-
类型
-
RetentionPolicy.SOURCE
- 注解将被编译器丢弃(class文件中没有)
-
RetentionPolicy.CLASS
- 注解在class文件中可用,但会被JVM丢弃(内存没有)
-
RetentionPolicy.RUNTIME
- JVM在运行时,也会保留注解信息
-
注解默认情况下的保留级别是CLASS(运行时已经没了)
-
-
-
@Target
-
该注解用来声明和限定注解使用的地方
-
注解可以定义在如下位置
-
定义在类上
- ElementType.TYPE
-
定义成员变量上
- ElementType.FIELD
-
定义在构造方法上
- ElementType.CONSTRUCTOR
-
定义在成员方法上
- ElementType.METHOD
-
可以同时声明,注解使用在多个位置
-
-
GC
jvm运行时数据区域
程序计数器
- 线程私有(线程隔离)
Java虚拟机栈
- 线程私有(线程隔离)
本地方法栈
Java堆
- 多线程共享
方法区
- 多线程共享
jvm的内存管理
显式内存管理(C/C++)
-
内存管理(内存的申请和释放)是程序开发者的职责
-
常见问题
- 内存泄漏:内存空间已经申请,使用完毕后未主动释放
- 野指针:使用了一个指针,但是该指针指向的内存空间 已经被free
隐式内存管理(Java/C#)
-
内存的管理是由垃圾回收器自动管理的
-
优点
- 增加了程序的可靠性,减小了memory leak
-
缺点
- 缺点:无法控制GC的时间,耗费系统性能(stop the world),无法消除内存泄漏
garbage collection
如何确定“垃圾”
-
引用计数法
-
过程
- 给对象添加一个引用计数器
- 每当一个地方引用它时,计数器加1
- 每当引用失效时,计数器减少1
- 当计数器的数值为0时,也就是对象无法被引用时,表明对象不可在使用
-
缺陷
- 无法解决对象间的循环引用的问题
-
-
跟搜索算法
-
这个算法的基本思想是将一系列称为“GC Roots”的对象作为起始点
-
从这些节点开始向下搜索
-
搜索所走的路径称为引用链
-
当一个对象到所有的GC root之间没有任何引用链相连,时,就认为该对象变成了垃圾
-
GC Roots包含对象呢?
- 虚拟机栈中引用的对象
- 方法区中的静态属性引用的对象
-
如何回收垃圾
-
标记清除算法(Mark Sweep)
-
标记复制算法(Copy)
-
标记整理算法(Mark Compact)
-
分代收集算法
- 虚拟机中真正使用的方式
何时触发GC
- 申请heap space失败后会触发CG回收
- 系统进入idle后一段时间会进行回收
- 主动调用GC进行回收
GC相关概念
-
Shallow size
- 就是对象本身占用的内存大小,也就是对象头加成员变量
占用内存大小的总和
- 就是对象本身占用的内存大小,也就是对象头加成员变量
-
Retained size
- 是该对象自己的shallow size 加上仅可以从该对象访问(直
接或者间接访问)的对象的shallow size之和。 - 是该对象被GC之后所能回收的内存的总和。
- 是该对象自己的shallow size 加上仅可以从该对象访问(直
内存相关问题
Out of Memory内存溢出
- Heap OOM 堆溢出
- Stack Overflow 栈溢出
内存溢出 VS 内存泄漏
- 内存泄露可能导致内存溢出,但不是必然导致内存溢出