第二章 JAVA语言基础与流程控制
1.JAVA语言标识符的规则
①标识符由字母、数字、下划线和美元符号组成的字符串
②第一个字符必须为:字母、下划线或$
③标识符不能是关键字
④标识符不能是true、false和null
⑤标识符可以有任何长度
⑥Java标识符区别字母的大小写
注:按照习惯,变量名用小写字母。如果一个名字由多个单词构成,则除第一个单词外,其它单词首字母大写,这种命名方法称为驼峰式命名法,按照习惯,定名常量用大写字母,名字由多个单词组成时,用下划线连接。
2.数值型数据类型的级别
byte short char int long float double(从左到右 从低到高)
当从级别低的数据类型到高的数据类型转换时,可以完成自动类型转换
当从级别高的数据类型到低的数据类型转换时,需要完成强制类型转换
字符串转换成数值型数据
Ingerger.parseInt(String s):将数字字符串转换为整形数据
Double.parseDouble(String s):将数字字符串转换为双精度型数据
3.不同类型数据算术运算时的转换规则
①如果运算对象之一是double,就将另一个转换为double型(因为都可以完成自动类型的转换)
②否则,如果运算对象之一是float,就将另一个转换为float型。
③否则,如果运算对象之一是long,就将另一个转换为long型。
④否则,两个运算对象都转换为int型
总之就是往高级别的进行转换
4.switch语句
switch-expression的计算结果必须是char、byte、short或int,value1, …,valueN的类型必须与switch-expression的类型相同.
第三章 数组与字符串
1.数组的基本概念
(1)栈内存:在函数中定义的一些基本类型的变量数据和对象的引用变量都在函数的栈内存中分配。当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当该变量退出该作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。
(2)堆内存:
①堆内存用来存放由new创建的对象和数组。 在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理
②引用变量是普通的变量,定义时在栈中分配,引用变量在程序运行到其作用域之外后被释放。
③而数组和对象本身在堆中分配,即使程序运行到使用 new产生数组或者对象的语句所在的代码块之外,数组和对象本身占据的内存不会被释放,数组和对象在没有引用变量指向它的时候,才变为垃圾,不能在被使用,但仍然占据内存空间不放,在随后的一个不确定的时间被垃圾回收器收走(释放掉)。这也是 Java 比较占内存的原因。
④实际上,栈中的变量指向堆内存中的变量,这就是 Java中的指针!
2.数组的初始值
数组创建后, 其元素有默认值
数值型默认为: 0
char型默认为: ‘\u0000’
boolean型默认为: false
3.foreach循环
for-each循环使用元素变量顺序、只读的访问整个数组
for(元素类型 元素变量:数组变量){
do something here;
}
4.数组的克隆
用循环语句复制数组
int[] b = new int[a.length];
for(int i=0; i<a.length; i++) { b[i] = a[i]; }
使用System类的arraycopy方法
int[] b = new int[a.length];
System.arraycopy(a, 0, b, 0, a.length);
使用数组对象的clone方法复制数组
int[] b = a.clone();
5.字符串的常用构造方法
使用字符串直接量构造, 例如:
String message = new String("welcome to Java");
字符串的快捷初始化方法, 例如:
String message = "welcome to Java";
使用字符数组构造, 例如:
char[] charArray = ['G', 'o', 'o', 'd', ' ', 'D', 'a', 'y'];
String message = new String(charArray);
6.字符串(String)的特点
①String类用来描述和处理字符串类型
②所有的字符串直接量, 例如"abc", 都是String类
③String类的对象是不可变对象, 即它们的值在对象创建后就不能改变了
④Java把相同的字符串直接量存储为一个对象(规范字符串).
7.字符串更新方法
由于String对象是不可变的,因此对String对象进行修改操作时,原字符串不变,生成一个新的String对象。
• String trim()
• String replace(char oldChar, char newChar)
• String replaceFirst(String oldString, StringnewString)
• String replaceAll(String oldString, StringnewString)
• int length()
• boolean equals(Object anObject)
• String substring(int beginIndex)
• String substring(int beginIndex, int endIndex)
• char chatAt(int index)
• int indexOf(String str)
• int lastIndexOf(String str)
• int compareTo(String anotherString)
第四章 类与对象
1.类与对象的基本概念
封装性:通过抽象找出具体实例中的共同特征(数据和操作),将数据和对数据的操作封装在一起
继承性:子类可以继承父类的数据及数据操作,同时又可以扩展子类独有的数据以及数据操作
多态性:①基于重载实现的静态多态:多个操作具有相同的名称,但是接收不同的消息类型
②基于继承和覆盖的动态多态:同一操作被不同类型的对象所调用而产生不同的行为
对象:代表现实世界中可以明确标识的一个整体。
对象的状态(State)由具有当前值的数据域(data field)的集合构成
对象的行为是方法的集合定义而成
类定义了同一类型的对象。它定义了该类对象的数据域和方法
实例化:由类的定义创建对象
2.JAVA类的结构
(1)类的命名:类名使用小写字母,每个单词的首字母大写(类的首字母也是大写的 )
(2)数据域的命名:从第二个单词的首字母开始大写
(3)数据域的有效范围:被其他数据域引用时的数据域必须先声明
(4)局部变量:定义在方法中,从该变量的说明开始到包含该变量的块体结束为止
(5)成员变量:定义在类中,局部变量与成员变量同名时,在方法中局部变量优先
3.构造方法与对象的创建以及使用
(1)定义:构造方法是一种特殊的方法,在创建对象时调用
构造方法必须与所在的类有相同的名字
构造方法必须没有返回类型,void也不可以
作用:对数据域内的变量进行初始化
(2)默认构造方法:当类中没有明确声明构造方法时,隐含一个方法体为空的无参构造方法
如果class前面有public修饰符,则默认的构造方法也会是public的
(3)方法重载式的构造方法:一个类可以有多个构造方法,通过方法重载实现,即多个构造方法的形式参数的个数或类型必须不同
重载的定义:方法与方法之间的参数个数不同,或参数的类型不同,便可以定义多个名称相同的方法
注:即使已经写了新的重载的构造函数,但是空方法体的构造函数还是需要保留的
(4)生命周期:在对象创建时自动调用并且执行
(5)从一个构造方法内调用另一个构造方法:从某一个构造方法内调用另一构造方法,是通过this()语句来调用的,this语句必须写在构造方法的第一句内
(6)数据域的默认初始化:如果数据域没有进行初始化,默认规则为引用型变量为null,boolean型变量为false,数值型为0,char型为’\u0000’
(7)对象的内存模型:基本上可以理解为引用对象是一个地址,这个地址指向了对象所存储的那一块空间
4.方法的参数传递
JAVA语言中,所有参数都是值传递,也就是发生方法调用时,方法调用语句中的"实际参数",把其值单向传递给形式参数,方法中修改形式参数的值,对"实际参数的值没有影响"
那么值得注意的是,如果使用的是引用类型的参数传递,那么实参会将(某个引用对象的地址)赋给形参,实参和形参会引用同一个对象实体
关于这一点,需要做一个代码实验:
//定义Circle类
public class Circle {
public double radius;
Circle(){
}
Circle(double radius){
this.radius=radius;
}
void getRadius(){
System.out.println(this.radius);
}
public static void swap(Circle c1,Circle c2){
c1.radius=2.0;
Circle temp=c1;
c1=c2;
c2=temp;
}
}
//使用Circle完成对象交换实验
Circle c1,c2;
c1=new Circle(2);
c2=new Circle(4);
System.out.print("c1:");
c1.getRadius();
System.out.print("c2:");
c2.getRadius();
c1.radius=3.0;
Circle.swap(c1,c2);
System.out.print("c1:");
c1.getRadius();
System.out.print("c2:");
c2.getRadius();
我们在这个代码实验中,创建了两个圆对象,C1和C2,在代码中试图改变c1的radius的值,又在这个操作后接了一个swap操作,在这个swap中又修改了c1.radius值,运行后结果为
c1:2.0
c2:4.0
c1:2.0
c2:4.0
容易看出,在swap中确实完成了对对象的引用修改,而且是直接到内存的修改,但是为什么没有交换呢?这是因为传进去的是对象引用量,在参数中形成了一个新的对象来接收这个引用,而在原来的main函数中的对象是同一个引用,因此是没有被交换到的
5.实例成员与类成员
1.静态成员
(1)修饰关键字:static,可以用来修饰静态成员,也可以用来修饰静态方法
(2)成员生成以及内存空间分配:静态变量在类加载到内存时分配内存空间。使用new运算符创建对象时,各个对象变量共享已经存在的静态变量的内存空间
(3)变量访问方式:对静态变量的使用方式是类名.静态变量
(4)方法访问方式:类加载到内存时,为静态方法分配内存空间,在静态方法中不能访问未创建对象的实例成员
2.实例成员
(1)修饰关键字:未用static修饰是实例成员
(2)成员生成以及内存空间分配:实例变量只有其对象生成后才会分配空间,使用new运算符创建对象时,每个对象的实例占用各自不同的内存空间
(3)变量访问方式:对实例变量的使用方式为所引用的对象名.实例变量
(4)方法访问方式:引用变量.实例方法(),当创建类的对象时,实例方法才会分配入口地址。注意,多个对象的同一实例方法的入口地址是共享的,在实例方法中可以直接访问实例成员和类成员
6.静态初始化器与实例初始化器定义
初始化器:是直接在类中定义的用一对{}括起来的语句组
·静态初始化器:使用static关键字修饰,用来初始化静态变量
·实例初始化器:没有修饰关键字,用来初始化实例变量,一个类可以有0个或多个静态和实例初始化器
//实例初始化器
{
this.radius=2;
}
//静态初始化器
static {
R=2;
}
7.静态初始化器与实例初始化器执行
静态初始化器的执行:类首次加载到内存时,首先是静态数据域变量的初始化,然后按排列顺序执行类中static初始化器
实例初始化器的执行:每次使用new构造方法(),创建对象时,首先是实例数据域变量的变量初始化,然后开始执行本类的构造方法,但在构造方法第一条语句执行之前,按排列顺序执行类中的实例化初始化器,然后执行构造方法的剩余语句
8.方法重载
定义:一个类中2个或更多的方法具有相同的名称但不同的形参列表,称为方法重载
被重载的方法的形参必须不同
①形参个数不同
②形参的类型不同
③形参的排列顺序不同
当进行方法调用时, Java编译器寻找最合适的匹配方法调用(可以存在模糊的重载)
不能基于方法的修饰符和返回值进行重载,也就是不能改变修饰符和返回值进行重载,这个不叫重载
9.对象的组合
一个类的成员变量可以是Java允许的任何类型,当然也包括其它类的对象类型。
例如类A使用类B的对象作为成员变量,则A的对象把B的对象作为组成部分。称为:A has-a B。
关于这个知识点,可以学习UML图
学习链接:点这里学习UML图以及组合关系
第五章 类的特性
1.包的使用
(1)为何使用包?:JAVA中使用包来对类进行组织和管理
使用包的优点有:①查找定位类②避免命名冲突③便于发布软件④保护类
(2)包的创建以及命名习惯:包是有层次关系的,一个包中还可以有包,称为子包,通常子包都采用小写字母来命名
(3)包与目录:JAVA中,包名与文件系统的目录结构一一对应
(4)将类打包到包里的方式:JAVA中每个类都属于某一个包,类在编译时被添加到包中。默认情况下,类在编译时放在当前目录(默认包)
package cn.scau.mypackage;//将当前类按照此目录结构保存
(5)引入包(使用包中的类):①使用类的全称②使用import语句来进行类的引入
import java.lang.util.*;
注:import语句位于package语句之后,类定义之前
2.访问权限
(1)定义:访问权限是指限制在一段代码中能否访问一个类能否通过"."访问的类中定义的方法或成员变量
JAVA语言使用3个关键字实现了4种访问权限
(2)各修饰关键字所对应的访问权限
public:类、方法、成员变量。访问范围:公开范围,也就是在应用程序中任何位置均可
private:方法、成员变量,(注:内部类可以用private来修饰)。访问范围:被修饰目标所在类中可以访问
protected:方法、成员变量。访问范围:继承的类可以访问具有protected属性的方法以及成员变量,
缺省访问控制符(也就是什么都不加):类、方法和成员变量,访问范围,同一个包内
3.访问权限的使用-数据域封装
作用:把对数据域的直接访问变为间接访问
实现:①使用private修饰数据域②为每个数据域创建访问器方法和修改器方法
访问器方法:
public 返回类型 get属性名();
public boolean is属性名();(个人认为这个方法可有可无,因为定义很模糊)
修改器方法:public void set属性名();
4.构造方法的访问权限
只在本类内部创建该类对象使用private修饰构造方法
在其他类创建对象使用public或其他访问权限修饰构造方法
默认构造方法的访问权限与类的访问权限相同
类的所有构造方法均为private时,无法在外部创建对象
第六章 子类与继承
1.子类与父类、子类的继承性
继承:在面向对象程序设计中,可以从已有的类派生出新类,称为继承
事实上,JAVA程序中的每一个类均显式或者隐式的派生自一个已存在的类
没有指明来源的类隐含由java.lang.Object派生,它是java程序中所有类的公共起源,它自身没有继承其他类
定义:设C2类由C1类派生而来
C2称为:子类(child class)、次类(sub class)、扩展类(extend class) 、派生类(derived class)
C1称为:父类(parent class)、超类(super class)或基类(base class)
子类的创建
//父类的定义
public class test
{
private int num;
public test(){
}
test(int num){
this.num=num;
}
public void saySomeThing(){
System.out.println("this is test's "+num);
}
}
//子类的继承写法
public class test2 extends test{//JAVA支持单继承,也就是子类只能继承自一个父类
}
关于继承的理解
子类继承父类的成员变量和方法是指子类可以将它们作为自己定义的成员,访问权限保持不变
子类继承父类有两个方面的含义:
继承父类可访问的数据域与方法:
①子类与父类在同一包中,继承:public,protected、默认
②子类与父类在不同包中,继承:public、protected
扩展添加新的数据域与新的方法
注意:子类不继承父类的构造方法和不可访问的成员(private),但是可以通过特定方式间接使用他们
1.2子类的构造方法
①子类不继承父类的构造方法,子类构造方法中必须调用父类的构造方法
②调用父类构造方法的语句必须出现在子类构造方法的第一句
③若子类构造方法没有显式调用父类构造方法,则编译器把super()作为子类构造函数的第一句
④子类的构造方法也可以调用本类中重载的其他构造方法(this),此时构造方法第一句不会是super(),super(参数),但是要保证任何一个子类构造方法都能调用父类构造方法
⑤构造方法调用链:构造一个类的实例时,会沿着继承链依次调用每个父类的构造方法,称为构造方法调用链,注意,由于每个类的父类都是object类,因此顶端是object所规定的构造方法,调用次序是从最上级的父类开始调用。这里的理解就是,由于父类的构造方法是为子类内的成员变量进行初始化,因此如果子类的直接父类也有父类,那么这个直接父类将作为子类这一角色,使用其父类的构造方法,为它自己的数据域内的成员变量进行初始化
1.3方法覆盖(overriding)
2.子类继承父类的实例方法,可以根据以下规则重写该方法:
①子类定义一个与继承实例方法同名的方法
②要求:与父类方法返回类型相同或是返回类型的子类,子类方法的形参的个数、类型必须与父类方法完全相同。参
数名称可以不同。
③访问权限:子类方法的访问权限必须大于等于父类方法的访问权限。
④静态方法的重写:子类中可以按照同样的规则重写父类中的静态方法,但这种语法不是覆盖。
注:在子类中覆盖父类的方法时,可以扩大父类中的方法权限,但不可以缩小父类方法的权限。
1.5使用super访问父类的实例成员
关键字super在子类中使用,主要有两种用途:
①调用父类的构造方法
②访问已经继承的成员变量和实例方法
1.6 final关键字总结
在默认情况下,所有的成员变量成员方法都可以被覆盖,如果父类的成员不希望被子类的成员所覆盖可以将他们声明为final。如果用final来修饰成员变量,则说明该成员变量是最终变量,也就是常量,程序中其他部分都可以访问,但是不可以修改。如果用final修饰成员方法,则该成员方法不能再被子类所覆盖,也就是该方法是最终方法。
如果一个类被final修饰符所修饰,则说明这个类不能再被其他类所继承,即该类不能有子类,这种类称之为最终类
①final类不能被继承
②final方法不能在子类中被重写(覆盖)
③final变量的值不能改变
1.7对象类型转换
①向上类型转化(向上造型):父类类型的对象引用可以引用一个父类对象或者任何的子类对象,称为隐式类型转化(向上类型转换)
简单来说就是用一个数据域和方法更少的来接受更多数据域和方法,这样不会导致访问到不存在的内存,因此向上类型转换总是安全的。
②向下类型转化(向下造型):如果没有显式地强制类型转换, 一个子类类型的对象引用不可以引用父类对象(个人理解是存在空域,因为子类可能存在扩展父类的东西). 把父类类型强制转换为子类类型称为向下强制类型转换。
③如果两个类类型是平等的,即任何一个都不是另一个的祖先或后代,则这两个类类型不能进行类型转换。
1.8 instanceof运算符
instanceof 运算符语法:对象 instanceof 类
运算结果:boolean
运算规则:如果对象的类是后面的类或其子类,返回true;否则返回false。
精确判断一个对象的类:
对象.getClass() == 类名.class
2.多态及其应用
(1)多态的定义:多态即“一个接口,多种实现”,在父类声明的方法,可以在子类中进行覆盖(声明为final的除外),这样,父类的引用在引用子类对象的时候可以做出不同的响应。因此,多态也可以被理解为:相同的消息被发送到子类或父类对象上,将导致完全不同的行为。
(2)多态性:为父类设计的任何代码都可以引用于子类,这个特征称为多态性
(3)程序中多态的实现:①多态在类的继承层次结构出现 ②表现多态的形式是子类覆盖父类的实例方法 ③通过父类的引用去访问继承层次中不同类的对象,调用覆盖的方法时产生多态
(4)对于静态方法:如果子类重写父类的静态方法,子类对象的向上转型对象不能调用子类静态方法, 只能调用父类静态方法。即:多态性只针对实例方法
(5)为什么会出现多态:Java 中的引用变量有两个类型,一个是编译时的类型,一个是运行时的类型,编译时的类型由声明该变量时使用的类型决定,运行时的类型由实际赋给该变量的对象决定。如果编译时的类型与运行时的类型不一致就会出现所谓的多态。
3.数据域及其隐藏
①可以覆盖一个实例方法, 但不能覆盖一个数据域(实例或静态)或静态方法.
②如果在子类中声明与父类同名的数据域或静态方法,则父类的被隐藏, 但依然存在, 在子类中可以使用super去引用被隐藏的数据域或静态方法.(就类似于this与局部变量的使用与区分)
③使用引用变量访问实例方法时, 所引用的实际对象的类在运行时决定调用该方法的哪个实现.
④访问数据域或静态方法时, 由声明引用变量的类在编译时决定使用哪个数据域或静态方法
4.抽象类
(1)抽象方法 : 只有方法头没有方法体.用abstract修饰
(2)抽象类:用abstract修饰的类, 不能用new创建对象.
(3)抽象类与抽象方法的相关说明:
①包含抽象方法的类必须是抽象的, 但也允许声明没有抽象方法的抽象类.
②抽象类不能用new运算符实例化, 但包含构造方法.
③具体的父类也可以派生抽象的子类.
④子类可以将父类的具体方法覆盖为抽象方法
⑤抽象类不能创建对象, 但可以声明对象引用变量.
5.接口
1.接口的定义
[public] interface 接口名称 [extends 父接口1, ..., 父接口n] {
[public] [static] [final] 数据类型 常量名 = 值;
[public] [abstract] 返回类型 方法名(形参表);
[public] static 返回类型 方法名(形参表) { ... }
[public] default 返回类型 方法名(形参表) { ... }
}
说明:
①interface前的public省略时,接口的访问范围为package.
②interface的extends部分省略时,没有默认的父接口
③接口成员前面的public无论是否省略,均为public.
④接口成员的abstract省略后该方法依然是abstract方法.
⑤接口源程序文件命名规则与类相同, 接口名.java
⑥接口在编译时生成一个.class, 接口不能用new创建实例
2.接口的实现与引用
一个类可以extends一个父类, 同时implements多个接口.
class 子类 extends 父类 implements 接口1, …, 接口N {
……
}
说明:
①子类需要Override其实现接口的抽象方法,如果没有全部覆盖(实现),则子类需要使用abstract修饰。
②接口中的抽象方法均为public,因此子类在覆盖接口的抽象方法时,形式上要求方法头完全相同。
③接口可以声明变量,称为接口变量。
④接口变量可以存放一个对象的引用(地址), 要求该对象的类实现对应接口。
接口回调:
①定义:指把实现了某个接口的类的对象的引用赋值给该接口声明的接口变量。
②通过接口变量去调用被类实现的接口的方法
③通过接口变量无法调用类中非实现接口中声明的方法。
接口回调的代码示例:
首先定义接口
package Test;
public interface MoveAble {
public abstract void moving();
}
实现该接口
package Test;
public class Implement implements MoveAble{
@Override
public void moving(){
System.out.println("I can moving");
}
}
声明接口变量,并且将实现了接口的类回调给它
MoveAble moveAble = new Implement();
3.关于接口多重继承的名字冲突问题
①如果子接口中定义了与父接口同名的常量或者相同的方法,则父接口中的常量被隐藏,方法被覆盖。
②在接口的多重继承中可能存在常量名或方法名重复的问题,即名字冲突问题。
③对于常量,若名称不冲突,子接口可以继承多个父接口中的常量,如果多个父接口中有同名的常量,则子接口不能继承,但子接口中可以定义一个同名的常量。
④对于多个父接口中存在同名的方法时,此时必须通过特殊的方式加以解决:
解决多个父接口中存在同名的冲突问题,有二种方法:
提供同名方法的一个新实现;
委托一个父接口的默认方法。
如果两个父接口中有一个提供的不是默认方法,而是抽象方法,则只需要在接口的实现类中提供同名方法的一个新实现即可。
如果两个父接口中的同名方法都是抽象方法,则不会发生名字冲突,实现接口的类可以实现该同名方法即可,或者不实现该方法而将自己也声明为抽象类。
如果一个类继承一个父类并实现了一个接口,而从父类和接口中继承了同名的方法,此时采用“类比接口优先”的原则,即只继承父类的方法,而忽略来自接口的默认方法。
4.关于接口继承的问题
可以在接口中定义用static修饰的静态方法。接口中的静态方法与普通类中静态方法的定义相同。
接口中的静态方法不能被子接口继承,也不能被实现该接口的类继承。
接口中的默认方法用defualt修饰符进行修饰,默认方法可以被子接口或被实现该接口的类所继承,但若子接口中定义名称相同的默认方法,则父接口中的默认方法被覆盖,在实现类中可以继承默认方法,也可以根据需要重新实现默认方法。接口中的默认方法虽然有方法体,但是不能够通过接口名进行直接访问,但是可以通过接口实现类的实例进行访问。
关于default方法的使用,贴一个连接:default方法的使用
第七章 异常处理
1.理解异常与错误
(1)异常处理的基本概念:异常是指程序运行中由代码产生的一种错误。在不支持异常处理的程序设计语言,每一个运行时错误必须由程序员手动控制,这样不仅给程序员增加很大的工作量,而且这种方法本身也是很麻烦的。
(2)错误的类型:按照错误的性质可以将错误分①语法错误(程序中违反了语言的规则、可以由编译器发现)②逻辑错误(程序没有按照预期的方案执行,通过测试和调试程序来发现和改正)③运行错误(或者称为语义错误)(程序运行过程中,运行环境发现了不可执行的操作,JAVA通过异常处理来解决运行错误)
(3)异常发生时:当产生异常时,程序的正常执行流程会中断,JAVA给程序员处理运行异常的功能,称为"异常处理".
(4)异常抛出:在发生异常事件处产生一个异常对象,并交给运行环境
(5)异常抛出:异常抛出后,从生成异常对象的代码开始,沿着方法调用栈回溯查找,直到找到异常处理的方法,称为异常捕获
(6)异常处理机制:JAVA语言中定义了很多异常类,每个异常类都代表一种运行错误,类中包含了该运行错误的信息和处理错误的方法等内容。每当JAVA程序运行过程中发生一个可识别的运行错误,即该错误有一个异常类与之对应时,系统都会产生一个相应的该异常类的对象。一旦一个异常对象产生了,系统中就一定有相应的极值来处理它,从而保证整个程序的运行的安全性。异常本质上是一个在程序执行期间发生的事件,这个事件将中断程序的正常运行。当在JAVA方法内部发生异常时,这个方法就创建一个异常的对象,并把它传递给运行时环境(JRE)。创建一个异常对象并把它传递给JRE的过程就是抛出一个异常。JRE从异常发生的方法开始查找异常处理程序,如果异常处理捕获到的异常类型和这个程序能够处理的类型相同,那么这个程序叫做合适的异常处理程序,然后异常处理机制将控制权从发生异常的程序交给能处理该异常的异常处理程序,如果没有找到合适的异常处理程序,JRE将终止程序运行
2.异常类
JAVA异常是java.lang.throwable的后代类的对象实例
系统错误:由JVM抛出并在Error类中描述,很少发生,如果发生只能通知用户尽量稳妥地结束程序
运行异常:由RunTimeException类描述的编程错误,由JVM抛出,如数组下标越界,空指针操作、类型转换失败等
异常:由Exception类进行描述,由程序和外部环境引起的错误,能通过程序进行捕获和处理
关于Error类与Excepiton类的区别:
(1)异常的分类:免检异常(unchecked exception)如上图所示、必检异常
(2)异常对象所包含的信息
public String getMessage()
返回异常对象的详细信息
public String toString()
返回: 异常类名全称 + “:” + getMessage()
public void printStackTrace()
在控制台输出异常对象及跟踪信息
public StackTraceElement[] getStackTrace()
返回该异常对象相关的堆栈跟踪元素的数组
3.异常处理
(1)异常的抛出:程序代码运行中出现了错误,由JRE或程序代码创建并抛出异常对象
(2)异常的捕获和处理:异常对象在方法调用栈中传递,并使用try-catch语句进行捕获和处理。处理后异常消失,如果最终没有捕获和处理则由JRE终止程序运行
注:JRE指的是java运行环境
(3)异常处理细节:
①如果try块中某条语句抛出了异常,则跳过try块中剩下的语句
②程序按照catch子句的排列顺序中逐个检查其参数类型是否与抛出异常的类型匹配
③如果有匹配的catch子句,则将异常对象传递给该子句的参数,并且执行其中的语句
④如果没有匹配的catch子句, 则Java退出方法, 把异常对象传递给调用该方法的方法并重复以上过程.
⑤如果最终不能找到合适的处理器,则终止程序运行并显示错误信息
⑥任何情况下,finally内的语句块都一定会执行
3.1抛出异常
在捕获一个异常之前,必须有一段代码生成一个异常对象并把它抛出。根据异常类型的不同,抛出异常的方法也不相同,可以分为:
(1)抛出异常的方式:①由系统自动抛出异常 ②指定方法抛出异常
所有系统定义的运行时异常都可以由系统自动抛出,而指定方法抛出异常需要使用关键字throw或throws来明确指定在方法内抛出异常。如用户程序自定义的异常不可能依靠系统自动抛出,这种情况就必须借助于throw或throws语句来定义何种情况算是产生了此种异常对应的错误,并应该抛出这个异常
抛出异常的方法
如果在一个方法内部的语句执行时可能引发某种异常,但是无法确定如何处理,则此方法应声明为可能抛出异常,表明该方法将不对这些异常进行处理,而由该方法的调用者负责处理(调用者处理)。一个方法声明抛出异常有如下两种方式
public void exceptionTest ()throws IOException {//方法1:在方法头部添加throws子句表示方法将抛出异常。当异常类多于一个时,就要用,隔开
String pathName="";
FileReader fileReader = new FileReader(pathName);
if(fileReader==null){
throw new IOException();//方法2:在方法体内使用throw抛出一次异常对象
}
}
处理异常的方法
由一个方法抛出异常后,该方法内又没有处理异常的语句,则系统就会将异常向上传递,由调用它的方法来处理这些异常,若上层调用方法中没有处理异常的语句,则可以再往上追溯到更上层,一直到main()方法,此时JVM是需要处理的,如此编译就可以通过了。如果某个方法声明将抛出异常,则调用它的方法必须捕获而且处理异常,否则将会出现错误
交给JVM的方式则是在main方法头加上抛出异常的声明
可以总结为调用者处理与被调用者处理
捕获异常和处理异常的注意事项
①一个异常父类可以派生若干子类, 捕获异常父类的catch子句可以捕获其所有子类的异常对象.
②catch子句的排列顺序非常重要, 不能将父类的catch子句排列在子类catch子句之前.
③Java强制要求程序员处理必检异常, 如果一个方法中某些语句可能抛出必检异常, 则:
该方法中处理该必检异常, 即使用try-catch语句.
该方法必须声明抛出该异常, 即方法声明中使用throws关键字声明该异常.
④对于RuntimeException及其子类,尽量不使用try-catch,而是使用检测来避免异常发生。例如:
3.2重新抛出异常
如果捕获并处理该异常, 则异常对象被清除.此时如果希望继续将异常对象传递给调用该方法的方法, 则需要将异常对象重新抛出. 语法为:
try {
语句;
}
catch(Exception ex) {
处理异常语句;
throw ex; //异常对象的重新抛出
}
第八章 文件管理与输入输出流
1.File类
绝对文件名:是由驱动器字母、完全路径和文件名组成,与操作系统有关。
java.io.File类描述一个目录或文件对象
构造方法
①File(String pathName)通过将给定路径名字符串转换成抽象路径名来创建一个新 File 实例。
②File(File parent, String child)根据 parent 抽象路径名和 child 路径名字符串创建一个新 File 实例。
③File(String parent, String child)根据 parent 路径名字符串和 child 路径名字符串创建一个新 File 实例。
常用方法
Scanner in = new Scanner(System.in);
String pathName;
pathName=in.nextLine();
File newFile=new File(pathName);//创建一个File的对象
System.out.println(newFile.getName());//返回由此抽象路径名表示的文件名或目录的名称
System.out.println(newFile.getPath());//将此抽象路径名转换为一个路径名字字符串
System.out.println(newFile.getAbsoluteFile());//返回抽象路径名的绝对路径名字字符串
System.out.println(newFile.getParent());
/*返回此抽象路径名的父路径名的路径名字符串,如果此路径名没有指定父目录,则返回 null。*/
文件目录属性相关的方法
boolean isDirectory():用来判断输入的路径是否为一个目录
boolean isFile():用来判断输入的路径是否为一个文件
文件信息相关的方法
long lastModified(),返回此文件最后一次被修改的时间。
long length(),返回由此文件的长度,字节为单位。
获取目录内容的方法
File[] listFiles()
返回目录中所有文件和子目录组成的数组。
使用示例:
File[] arrFile=newFile.listFiles();
for(int i=0;i<arrFile.length;i++){
System.out.println(arrFile[i].getName());
}
2.1字节流与字符流
以字节为传输单位而创建的流称为字节流
以字符为传输单位而创建的流称为字符流
2.2输入字节流和输出字节流(InputStream & OutputStream)
InputStream();//抽象类,字节输入流的父类
OutputStream();//抽象类,字节输出流的父类
流操作方法都声明抛出java.io.IOException或其后代类
2.3输入字符流和输出字符流
抽象类Reader是所有字符输入流的父类
抽象类Writer是所有字符输出流的父类
3.文件字节流
输入流的构造方法:
FileInputStream(File file) throws FileNotFoundException
FileInputStream(String path) throws FileNotFoundException
输出流的构造方法
FileOuputStream(File file) throws FileNotFoundException
FileOuputStream(String path) throws FileNotFoundException
因此,在构造流时,既可以以文件对象作为构造函数的参数,也可以传入字符串作为构造文件对象再进行进行流的构造
代码使用示例:
传统流的关闭方法
import java.io.*;
import java.util.Scanner;
import java.util.zip.InflaterInputStream;
public class Main {
public static void main(String[] s){
//实现文件的复制代码实现,传统关闭模式与自动关闭模式
Scanner in =new Scanner(System.in);
System.out.println("请输入将要复制的文件");
String srcPath;
srcPath = in.nextLine();
System.out.println("请输入将要输出的文件名(含路径)");
String dstPath;
dstPath = in.nextLine();
//对文件对象进行构造以及对流进行声明
FileInputStream srcStream =null;
FileOutputStream dstStream=null;
File srcFile=new File(srcPath);
File dstFIle=new File(dstPath);
try{
srcStream = new FileInputStream(srcFile);
dstStream = new FileOutputStream(dstFIle);
}catch (FileNotFoundException e){
e.printStackTrace();
}catch (IOException e){
e.printStackTrace();
}finally {
if(dstStream!=null){
try{
dstStream.close();
}catch (IOException e){
e.printStackTrace();
}
}
if(srcStream!=null){
try{
srcStream.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
}
}
使用try-catch以及while的读写方法
import java.io.*;
import java.util.Scanner;
import java.util.zip.InflaterInputStream;
public class Main {
public static void main(String[] s){
Scanner in =new Scanner(System.in);
System.out.println("请输入将要复制的文件");
String srcPath;
srcPath = in.nextLine();
System.out.println("请输入将要输出的文件名(含路径)");
String dstPath;
dstPath = in.nextLine();
try(FileInputStream srcStream= new FileInputStream(srcPath);
FileOutputStream dstStream = new FileOutputStream(dstPath) ){
int data;
while((data=srcStream.read())!=-1){
dstStream.write(data);
}
}catch (FileNotFoundException e){
e.printStackTrace();;
}catch(IOException e){
e.printStackTrace();
}
}
}
4.文件字符流
输出流的构造方法可以传入文件对象(抽象路径名称)也可以传入路径名字符串
FileReader(String name)
FileReader(File file)
输入流的构造方法也是对称的
FileWriter(String name)
FileWriter(String name,boolean append)
FileWriter(File file)
FileWriter(File file,boolean append):后一个参数用来表明是否想要追加,如果不希望覆盖则将其设置为true
5.关于流的异常处理方法
一般来说,输入流一般会抛出文件找不到的异常,输出流则一般会抛出IOException这个IO流的错误
6.缓冲流
以BufferedReader为和BufferedWriter为例
缓冲流是高级流,高级流对象必须建立在字节流和字符流之上,代码示例
String fileName;
Scanner inScanner= new Scanner(System.in);
fileName= inScanner.nextLine();
try{
BufferedReader in
= new BufferedReader( new FileReader(fileName) );
BufferedWriter out
= new BufferedWriter( new FileWriter(fileName) );
PrintWriter printWriter
= new PrintWriter( new BufferedWriter(
new FileWriter( fileName) ) );
}catch (FileNotFoundException e){
e.printStackTrace();
}catch (IOException e){
e.printStackTrace();;
}
除在try-catch内编写代码的方法之外,还可以直接在try中直接编写的方法
正则表达式与缓冲流使用综合案例
正则表达式:点这里学习
记住几个常用的:
①如这个前面的字符可以出现多次或者不出现,如abc这个东西,规定了a和c中间只能出现0或者多个b,+号则是能出现1或者多个,如果想要匹配多个字符的,那么就将该多个字符用括号括起来即可
②[a-z]代表所有的小写英文字符
③[a-zA-Z]代表所有的英文字符
④[a-zA-Z0-9]代表所有的数字和英文字符
⑤[ ^ ]则表示截取除了后面的东西比如[ ^ 0 - 9 ]就是截取除了数字之外的字符
⑥\d表示数字字符,\D则是表示非数字字符
⑦\w表示单词字符(英文、数字、下划线)
⑧\s表示换行符以及空格
⑨^会去匹配行首的,比如 ^ a会匹配行首的a
⑩$会去匹配行尾的,比如 $ a会匹配行尾的a
/*
* @author:Lumxi
* @date:2021-11-29
* @brief:file:输入的文件对象(抽象路径名),word:将要统计的单词
* */
public void count(File file,String word){
int numberOfLines = 0;
int numberOfWord = 0;
Pattern pattern = Pattern.compile("[\\W]" + word + "[\\W]");
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
String line = null;
// readLine()方法返回null,表示读取结束
while ((line = reader.readLine()) != null) {
numberOfLines++;
Matcher matcher = pattern.matcher(" " + line);
while (matcher.find()) {
numberOfWord++;
}
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
7.对象流
对象流也是高级流的一种,需要建立在基础文件流之上
ObjectOutputStream oos
= new ObjectOutputStream(
new FileOutputStream(fileName) );
作用:可以用来对文件内写入int、Object等参数
使用方法:
oos.writeInt(123456);//可以写入整形、string等普通变量
oos.writeObject(new Date());//可以写入对象变量
与输出的类似,还有输入的控制流
ObjectInputStream ois
= new ObjectInputStream(
new FileInputStream(fileName));
使用方法
int num=ois.readInt();//可以读取整形
Date now = (Date)ois.readObject();//可以读取具有对应格式的对象
String string = ois.readUTF();//以此方法读取字符串
8.对象序列化
在对象流的基础上,我们知道可以对文件内存储的数据进行对象的读取和写入操作,但是实际上这些对象需要经过序列化之后才能完成正常的读写操作
序列化的含义就是:将JAVA程序中的对象转化为字节序列的过程
反序列化的含义就是:将字节序列恢复为JAVA对象
一个类实现了 java.io.Serializable 接口,则该类的对象就是可序列化对象。
ObjectOutputStream代表对象输出流,它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。
ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。
注:使用writeObject() 和readObject()方法的对象必须已经被序列化
序列化操作的注意事项:
1.对象的类名、属性都会被序列化;而方法、static属性(静态属性)、transient属性(即瞬态属性)都不会被序列化(这也就是第4条注意事项的原因)
2.虽然加static也能让某个属性不被序列化,但static不是这么用的
3.要序列化的对象的引用属性也必须是可序列化的,否则该对象不可序列化,除非以transient关键字修饰该属性使其不用序列化。
4.反序列化地象时必须有序列化对象生成的class文件(很多没有被序列化的数据需要从class文件获取)
5.当通过文件、网络来读取序列化后的对象时,必须按实际的写入顺序读取。
一个类实现了 java.io.Serializable 接口,该类的对象组成的数组也是可序列化对象
ArrayList、HashSet等常用数组结构类也实现了java.io.Serializable 接口,因此如果其中的元素都是可序列化对象,则该数组结构对象也是可序列化对象
标记是否为可序列化的方法:
public class Main implements java.io.Serializable
不需要重写方法只需要加上implements修饰符来标记此类即可
第九章 注解、反射、内部类、Lambda表达式
1.注解
注解:注解也叫元数据,是用来描述数据的数据
描述:注解实际上上程序代码里的特殊标记,这些标记可以在编译、类加载、运行时被读取并且执行相应的处理
作用域:可以声明在包、类、接口、成员变量、成员方法、局部变量、方法参数等的前面,对这些程序元素进行注解说明
注意事项:通过注解,可以在不改变程序逻辑的情况下,在源程序文件中嵌入一些补充信息。然后对过反射机制编程实现对这些注解的访问
2.内部类定义
内部类:定义在其他类中的类
匿名内部类:是一种特殊的内部类,由于不会在其他地方用到这个类,因此没有类名
嵌套类:JAVA允许在类的内部定义类,可以分为两种,静态嵌套类(使用static修饰),非静态嵌套类:也称为内部类
创建内部类实例:
public class test {
static class StaticNetClass{
//something here
}
class InnerClass{
//something here
}
}
编译后的文件命名为:test.class、test $ StaticNetClass.class、test $ InnerClass内部类是用$来区分的
3.内部类的使用
1.关于变量的调用:在内部类对象中保存了一个对外部类对象的引用,当在内部类的成员方法中访问某一变量时,如果在该方法和内部类中都没有定义过这个变量,调用就会被传递给内部类中保存的那个对外部类对象的引用,通过外部类对象的引用去调用这个变量,在内部类调用外部类的方法也是一样的道理。值得注意的是即使这些成员变量和成员方法是private的,通过对内部类对外部对象引用,一样能够访问。
2.访问权限:一个嵌套类是其外部类的一个成员,其访问权限可以声明为public、private、protected或者包
3.关于静态嵌套内部类:静态嵌套内部类中不能够访问其外部类的实例成员,但是可以访问其外部类的静态成员。实际上当内部类被声明为static类型的时候,静态内部类将自动转化为“顶层类”(即它没有父类,而且不能引用外部类的成员或其他内部类中的成员),非静态内部类不能声明静态成员,只有静态内部类才能声明静态成员
4.外部类的访问:外部类中可以访问其嵌套类的所有成员,即使这些成员用private来修饰
内部类的特性:
①内部类前面用final修饰,则表明该内部类不能被继承
②内部类可以声明为abstract,但需要被其他的内部类继承或实现
③内部类名不能与包含它的外部类名相同
④内部类也可以是一个接口,该接口必须有另一个内部类来实现
注:由②④可知抽象类与接口都需要通过其他内部类来进行继承实现
⑤内部类不但可以在类中定义,也可以在程序块之内定义。例如,在方法中或循环体内部都可以定义内部类。但是方法中定义的内部类只能访问方法中的final类型的局部变量
⑥内部类既可以访问外部类的成员变量,包括静态和实例成员变量,也可以访问内部类所在方法的局部变量,匿名内部类不能够定义static的方法与成员变量
⑦独立定义的类称为顶层类,访问权限只有public和包两种
⑧在文件管理方面,匿名内部类在编译完成之后,所产生的文件名称为“外部类名$编号.class”,其中编号为“1,2,3,…,n”,每个编号为i的的文件对应于第i个匿名内部类。
4.使用内部类的原因
这是一种对只在一个地方使用的类进行逻辑分组的方式,如果类A只在类B中使用,那么A应该要成为类B的嵌套类。
这种方式能够提升封装,如果类A是类B的嵌套类,就可以使用更多的访问权限去封装地控制B的使用
5.匿名内部类的定义
将类的定义与对象的创建在一步内完成,即在定义类的同时就创建该类的一个对象,以这种方式定义的类不用取名字,所以称为匿名内部类,它实际上是一个没有名字的内部类
6.匿名内部类的定义
定义内部类时直接用其父类的名字或者它所实现的接口的名字
new TypeName(){
匿名类的类体
}
匿名内部类可以继承一个类或者实现一个接口,其中TypeName是匿名内部类所继承或它所实现的接口的名字,如果是实现一个接口,则该类是Object的直接子类,匿名内部类继承一个类或实现一个接口不需要使用extends或implements关键字。匿名内部类不能同时继承一个类又实现一个接口,也不能够实现多个接口。若匿名内部类实现的是接口,则调用的构造方法是Object(),
继承父类方式创建匿名内部类
定义
public abstract class test {
protected String name;
public test(String name){
this.name=name;
}
public abstract void hello();
}
继承父类方式
test first = new test("TOM") {//这个直接使用的父类的名称
@Override
public void hello() {
System.out.print("yes");
}
};
实现接口的方式
public interface test {
//定义接口
void sayHi();
}
//实现接口并且调用方法
test second=new test() {
@Override
public void sayHi() {
System.out.print("hello");
}
};
second.sayHi();
7.匿名内部类的特点
①匿名内部类必须是继承一个父类或实现一个接口,但不能够使用extends或implements关键字
②匿名内部类总是使用它父类的无参构造方法来创建一个实例。如果匿名内部类实现的是一个接口,那么调用的构造方法是Object()
③匿名内部类可以定义自己的方法,也可以继承父类的方法或覆盖父类的方法
④匿名内部类必须实现父类或接口中的所有抽象方法
⑤使用匿名内部类时,必然是在某个类中直接使用匿名内部类创建对象,所以匿名内部类一定是内部类,匿名内部类可以访问外部类的成员变量和方法
⑥匿名内部类中不能声明static成员变量和static成员方法
8.关于自己对匿名内部类的一些理解(不一定正确)
匿名内部类实际上就是引用了一个内部类,并且在外部类的代码段中实例化了这个内部类,而且这个内部类与一般类不同的是它没有名字,而且能在父类的基础上做出较大改动,那么这也意味着,假如有大量的方法差异的话可以使用匿名内部类简化代码。
9.函数式接口
函数式接口:首先函数式接口是一个接口,在接口里面只能有一个抽象方法,也称为SAM接口
Lambda表达式:是在应用函数式接口环境下一种简化定义形式,可以解决匿名内部类定义形式复杂的问题
//函数式接口的定义形式
@FunctionalInterface
public interface test {
void sayHi();
}
//函数式接口允许添加三种方法,重写的
@FunctionalInterface
public interface test {
void sayHi();
boolean equals(Object obj);//Object的public方法
default void doSomeMoreWork1(){
//do something
}
static void doSomeMoreWork2(){
//do something
}
}
10.Lambda表达式详解
理解:Lambda表达式是可以传递给方法的一段代码,可以是一条语句,也可以是一个代码块,因不需要方法名,所以说Lambda表达式是一种匿名方法,也就是没有方法名的方法,Lambda明确要求是在函数式接口上进行的一种操作,但是为了分辨出是Lambda表达式使用的接口,还是在接口的定义上进行@FunctionInterface的注解
代码实现
//接口定义
@FunctionalInterface
public interface test {
void sayHi(String name);
}
//接口实现
test t =(String)->{
System.out.println(String);
};
注:String是一个类型,用来指代将要传入的参数类型,(可以是随便的什么类型)然后把这个方法定义出来
Lambda表达式可以作为实参传递给方法。方法对应的形参必须是一个函数式接口类型。
public static String process(Function<String,String> func, String src){//传入的参数是Lambda表达式
return func.apply(src);
}
相当于学习数据结构时,所学到的那个定义函数方法指针的知识点
Lambda使用细节:
①单参数语法可以简化为
p1 -> {
statment1;
statment2;
//.............
return statmentM;
}
②lambda的单语句语法可以简化为(单语句可以不用花括号)
param1 -> statment
③lambda的return可以被省略(当只有单语句而且进行的简单的运算,而且原函数接口的定义需要对应的返回值)
(i)->2*i
public static void test(){
//Lambda有哪些语法?
//这里的实验都是用接口回调的方式来实现的
//1.无参数,无返回值
IntCalter intCalterPrint = ()->System.out.println("this is intCalter");
//2.有一个参数,并且无返回值
outOneNumber OutOneNumber = (x)->System.out.println(x);
//3.有两个参数,有返回值并且Lambda语句中有多个语句
ManyNumber manyNumber = (x,y)->{
int[] a= new int[2];
a[0]=x;
a[1]=y;
return a;
};
GenericInterfaceLambda <String>GIL = (here)->{
return here.getClass().getName();
};
intCalterPrint.saySomething();
OutOneNumber.outOne(1);
int[] a;
a = manyNumber.getManyNumber(1,2);
System.out.println(a[0]+"+"+a[1]);
System.out.print( GIL.getT("haha"));
}
第十章 泛型与容器类
1.泛型概念
(1)泛型:泛型是指数据的类型参数化,泛型实际上是在定义类、接口、方法时通过为其增加“类型参数”来实现的。即泛型所操作的数据类型被指定为一个参数,这个参数被称为类型参数,所以说,泛型的实质是将数据的类型参数化。
(2)实现方法:通过为类、接口、方法设置类型参数实现泛型
(3)优点:泛型使类或方法可以对不同类型的对象进行操作
一个类或方法可以处理多种类型对象可以使用类型转换的方式
public int compare(Object o1,Object o2){....}
上面的定义的方法compare的2个参数可以接受任何类型的对象
缺点:使用类型转换方式时,程序中使用对象实例时,需要进行向下强制类型转换,因此需要编写代码在程序运行时进行类型判断,来确保类型转换成功,不会发生ClassCastException
使用泛型的优点:在编译期间检查类型是否错误,而不是在运行期间检查
1.1泛型的定义
泛型类的定义:在类名后面加上
public class com <String>{
}
泛型接口定义:在接口名后面加上
public interface com <String>{
}
泛型方法定义
一般来说编写JAVA泛型方法时,返回值类型和至少一个参数类型应该是泛型,而且类型应该是一致的,如果只有返回值类型或参数类型之一使用了泛型,这个泛型方法的使用就会大大地收到限制,基本限制到跟不用泛型一样的程度。所以推荐使用返回值类型和参数类型一致的泛型方法。JAVA泛型方法广泛应用在方法返回值和参数均是容器类对象的情况
public static <E> void method (E element){
}
E就是泛型中的"类型参数",在类、接口或方法中定义类型参数E后,就可以在相应各个语句部分中使用参数E
注意:若泛型方法的多个形式参数使用了相同的类型参数,并且对应的多个类型实参具有不同的类型,则编译器就会将该类型参数指定为这多个类型实参所具有的"最近共同父类"直至Object
说明:一个static方法,无法访问泛型类的类型参数,所以如果static方法需要使用泛型能力,必须使其成为泛型方法,理解就是由于泛型类的类型参数在一开始的时候没有加载到内存中,而static的方法都是在一开始加载到内存的,那么这样就产生了一个冲突,static方法不知道你的T到底是什么,因此就产生了这样的一个问题。
当使用泛型类时,必须在创建泛型对象的时候指定类参数的实际值,而调用泛型方法时,通常不必指明参数的类型,此时编译器会找出具体的类型。
1.3泛型的应用
创建泛型对象时不传递类型,默认就会是object
com comObject = new com();
创建泛型对象时传递类型<>内的内容在jdk7后就可以省略
com<String> com1 = new com<>();
com<Integer> com2 = new com<>();
2.基本类型的类包装
基本类型值与包装类对象之间可以进行自动类型转换
基本类型 -> 包装类对象,称为装箱,例如:
Integer intObject = 2;
Integer intObject2 = new Integer(2);
包装类对象 -> 基本类型,称为拆箱
int initVal = new Integer(2);
1.4限制泛型的可用类型
定义泛型时,默认情况下任何类型都可以传递给泛型参数,如果要限制传入的类型,可以使用如下语法:
<T extends 类>传入类型是指定类或指定类
<T extends 接口>传入类型是指定实现了该接口的类
public class com <E extends Number>{//定义只存放数值类型
}
说明:
①无论是通过接口还是类来限制传入类型,都使用 extends 。
②当定义使用泛型的类、接口或方法对泛型传入的类型有限制要求时使用该语法。
1.5通配符
当需要在程序中使用同一个泛型对象名去引用不同的泛型对象时,就需要使用通配符?创建泛型类对象,但条件是被创建的这些不同泛型对象的类型实参必须是某个类或者是继承该类的子类又或者是实现某个接口的类。也就是说,只知道通配符"?"是某个类又或者是继承该类的子类又或者是实现某个接口的类,但具体是什么类型不知道。
JAVA支持向上自动类型转换,但是对于泛型参数,则不允许自动类型转换,如果定义泛型类
class Demo<T>{...//}
Demo<Animal> a = new Demo<Dog>(); // 语法错误
通配符?用于声明泛型类变量或方法形参,主要有三种用法
无边界的通配符: 泛型类名<?> var
固定上边界的通配符: 泛型类名<? extends T> var
固定下边界的通配符: 泛型类名<? super T> var
1.6 继承泛型类与实现泛型接口
Java定义类时可以继承一个父类或实现多个接口,父类可以是泛型类,接口可以是泛型接口。
不确定全部泛型的继承
class F1<T1,T2> extends GenericClass<T1,T2>{
@Override
public void test(T2 name){
}
}
确定部分泛型的继承
class F2<T2> extends GenericClass<Integer,T2>{
@Override
public void test(T2 name){
}
}
确定全部泛型的继承
class F3 extends GenericClass<Integer,String>{
@Override
public void test(String name) {
}
}
擦除类型,子类变为了非泛型类
class F4 extends GenericClass{
@Override
public void test(Object name){
}
}
子类可以添加自己的泛型
class F5<T1,T2,T3> extends GenericClass<T1,T2>{
T3 data;
@Override
public void test(T2 Name){
}
}
泛型接口的继承也是类似,就不赘述了,都是可以保留部分泛型类型
泛型使用的注意事项
①不能使用泛型的类型参数创建对象
public class Demo<T> {
T obj = new T(); //错误
}
②不能使用泛型的类型参数创建数组对象
public class Demo<T> {
T[] arr = new T[个数]; //错误
}
③不能在静态(变量、方法、初始化)使用泛型的类型参数
public class Demo<T> {
static T data; //错误
}
④定义异常类不能使用泛型
public class MyEx<T> extends Exception {} //错误
第十一章 多线程
1.多线程及相关的概念
现代操作系统支持多进程(process)和多线程(thread)。Java在语言层面对多线程提供支持
程序(program),是含有指令和数据的文件,存储在外部存储器,是静态的代码。
进程(process),是程序的一次执行过程,系统运行程序的基本单位,是动态的。
多任务(multi task),是操作系统可以同时运行多个进程,每个任务对应一个进程。
线程(thread),包含在进程中,是进程的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每个线程并行执行不同的工作。一个进程的多个线程共享该进程的资源和环境。
2.线程的概念
JAVA线程的状态:用Thread.State枚举类型来表示
NEW:新建尚未启动
RUNNABLE:在JVM中运行
BLOCKED:等待监视器锁定而被阻止运行
WAITING:无限期等待另一个线程执行特定操作
TIMED_WAITING:等待另一个线程执行操作达指定时间
TERMINATED:已退出的线程
说明:
线程任意时刻只能处于一个特定状态
这些状态是JVM线程的状态,不影响操作系统线程的状态
3.线程的优先级
Java 线程的优先级由低到高共10级,用整数 1~10 表示,Thread 类定义了3个常量:
Thread.MAX_PRIORITY, 最高优先级10
Thread.MIN_PRIORITY, 最低优先级1
Thread.NORM_PRIORITY, 默认优先级5
新建线程的优先级确定规则:
主线程的默认优先级是Thread.NORM_PRIORITY
新建线程继承其父线程的优先级。父线程是指创建新线程语句所在的线程,可能是主线程,也可能是某个用户创建的线程
使用线程对象的 setPriority() 方法自行设置线程的优先级。
4.线程的调度
线程的调度:是指在各个线程之间分配CPU资源。多个线程的并发执行是通过调度程序进行的。线程调度有2种模型:
分时模型:按照时间片为线程分配CPU资源,通常线程在分配的时间片用完之前不会主动放弃CPU资源。
抢占模型:线程获得CPU资源后会一直执行,线程可以由于执行结束或某种原因而主动放弃CPU资源。
实际的线程调度策略可能同时支持这2种模型。Java支持抢占式调度模型
5.Runnable接口
Runnable接口,所有线程类都直接或间接实现该接口。其源码如下:
@FunctionalInterface
public interface Runnable{
public abstract void run();
}
6.Thread类
Thread 类的构造方法:
Thread (Runnable target, String name):创建线程对象,线程名称为name,线程的 run 方法调用 target 的 run 方法。
7.线程的启动与结束
在 main 线程中创建两个线程对象线程启动并不是立即运行,等待被调度运行,线程只能 start 一次。多个线程是并行运行,运行次序由调度程序控制。
在当前线程中调用 “p.join()”,作用是暂停当前线程的执行,等待线程 p 执行结束后当前线程继续执行。这是一种线程的同步。
package Test;
import java.util.Scanner;
public class ThreadDemo{
public static void getSmallestNum(int n){
if(n<=1){
return ;
}
if(n>1){
int factor=0;
for(int i=2;i<=n;i++){
if(n%i==0){
factor = i;
break;
}
}
getSmallestNum(n/factor);
System.out.print(factor+" ");
}
}
public static void methodThread(){
//使用线程,必须先覆盖掉原来的run方法,run方法也就是在线程中将要执行的方法
//以编写一个递归函数为例
//(1)Thread (Runnable target, String name),使用Lambda表达式编写
Runnable runnable = ()->{
Scanner in = new Scanner(System.in);
System.out.print("请输入要计算的数:");
int x = in.nextInt();
getSmallestNum(x);
in.close();
};
Thread thread1 = new Thread(runnable,"thread1");
//(2)Thread()创建线程对象,线程名为thread-n
class Thread2 extends Thread{//编写内部类来进行使用
public Thread2(String name){//如果想使用父类的构造方法,则需要super一下
super(name);
}
@Override
public void run(){
System.out.println("hello!"+getName());
}
}
Thread2 thread2 = new Thread2("thread2");
//(3)Thread(name)或者Thread(Runnable r)则其线程名字为thread-n
Thread thread3 = new Thread("thread3"){//使用匿名内部类来实现
@Override
public void run(){
System.out.println("hello!"+getName());
}
};
Thread thread4 = new Thread( ()->{
System.out.println("hello!");//在这里使用Lambda的弊端是不能够获取相关信息
},"thread4");
//学习进程是如何运行的
try{
thread1.start();
thread1.join();//main线程暂停,等待thread1结束后继续执行
System.out.println();
System.out.println("线程"+thread1.getName()+"执行结束");
thread2.start();
thread2.join();//main线程暂停,等待thread2结束后继续执行
System.out.println("线程"+thread2.getName()+"执行结束");
thread3.start();
thread3.join();
System.out.println("线程"+thread3.getName()+"执行结束");
thread4.start();
thread4.join();
System.out.println("线程"+thread4.getName()+"执行结束");
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
8.线程间的数据共享
同一进程的多个线程可以共享相同的内存空间,实现多个线程之间的数据交换、实时通信和必要的同步操作。
一种实现线程间共享的解决方案:定义一个描述共享数据的类,创建该类的一个对象作为被共享数据,将共享数据的地址传递给线程对象,线程通过地址访问共享数据。
多线程程序设计(并发程序设计)中,多个线程共享的资源或数据称为“临界资源”,线程中访问(修改操作)临界资源的代码称为“临界区”。
为保证临界资源的数据安全,需要将临界区定义为不可中断的原子操作,任何时刻只能有一个线程的临界区执行,称为互斥。
9.多线程的同步控制
Java 多线程提供“互斥锁”实现互斥操作,使用 synchronized 关键字:
方式1:将部分代码声明为针对某个临界资源的临界区
Data data = new Data();
synchronized (data){//获得对象的锁后执行后面的代码在线程中得到这个锁并且操作
//dosomething
}
方式2:在类方法中提供锁
public static synchronized void sale(){
tickets--;
}
10.synchronized使用总结
synchronized 使用总结:
• 所有锁定同一对象线程之间以串行方式执行临界区代码。
• 临界区中的代码应尽可能少。
• 多个线程如果锁定的不是同一个对象,则临界区并发执行。
• 某线程对临界资源加锁后,其他线程可以调用该临界资源的非同步方法。
• 任何时刻,一个临界资源对象只能有一个线程对其加锁。
• 线程执行完临界区代码后,释放其对临界资源的锁。
• 临界资源对象中存放共享数据的数据域应为private 。
• 对临界资源的访问代码应该都在临界区中。
• static 方法中不能将其一部分代码设置为临界区。
• 使用 synchronized 修饰类时,该类所有访问均是 synchronized 方法。
11.线程通信
知识盲点补充
1.Comparable接口的实现
package Test;
public class com implements Comparable<com>{
private String comAddress;
public com(String s){
this.comAddress = s;
}
@Override
public String toString(){
return this.comAddress;
}
@Override
public int compareTo(com c){
if(this.comAddress.length() > c.toString().length()){
return 1;
}else if(this.toString().length() < c.toString().length()) {
return -1;
}
return 0;
}
}
如果该类实现了compareAble接口,那么该类就可以进行比较排序,排序的方法是
static public void main(String[] args){
com[] comArr = new com[]{
new com("a"),
new com("ab"),
new com("abc")
};
Arrays.sort(comArr);
for(int i=0;i<comArr.length;i++){
System.out.println(comArr[i].toString());
}
}
值得一提的是,也可以通过comarator比较器进行比较
package Test;
import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.Comparator;
class ComCompareable implements Comparator<com>{
@Override
public int compare(com c1,com c2){
if(c1.toString().length() > c2.toString().length()){
return 1;
}else if(c1.toString().length() < c2.toString().length()) {
return -1;
}
return 0;
}
}
public class Main {
static public void main(String[] args){
com[] comArr = new com[]{
new com("accccccccccccc"),
new com("ab"),
new com("abc")
};
Arrays.sort(comArr,new ComCompareable());
for(int i=0;i<comArr.length;i++){
System.out.println(comArr[i].toString());
}
}
}
这个就很类似于与CSTL里面的cmp重载方法
2.Clone接口的实现
package Test;
public class com implements Cloneable{
private String comAddress;
public com(String s){
this.comAddress = s;
}
@Override
public String toString(){
return this.comAddress;
}
@Override
public com clone() throws CloneNotSupportedException{
com cloneCom = new com(this.comAddress);
return cloneCom;
}
}
clone必须实现Cloneable接口。
3.static修饰的方法不能被重写可以被继承以及抽象类中不能有静态的抽象方法
4.关于重载和重写两个概念
重载
重载(overloading) 是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。
每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。
最常用的地方就是构造器的重载。
重载规则:
被重载的方法必须改变参数列表(参数个数或类型不一样);
被重载的方法可以改变返回类型;
被重载的方法可以改变访问修饰符;
被重载的方法可以声明新的或更广的检查异常;
方法能够在同一个类中或者在一个子类中被重载。
无法以返回值类型作为重载函数的区分标准。
重写
参数列表与被重写方法的参数列表必须完全相同。
返回类型与被重写方法的返回类型可以不相同,但是必须是父类返回值的派生类(java5 及更早版本返回类型要一样,java7 及更高版本可以不同)。
访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类的一个方法被声明为 public,那么在子类中重写该方法就不能声明为 protected。
父类的成员方法只能被它的子类重写。
声明为 final 的方法不能被重写。
声明为 static 的方法不能被重写,但是能够被再次声明。
子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为 private 和 final 的方法。
子类和父类不在同一个包中,那么子类只能够重写父类的声明为 public 和 protected 的非 final 方法。
重写的方法能够抛出任何非强制异常,无论被重写的方法是否抛出异常。但是,重写的方法不能抛出新的强制性异常,或者比被重写方法声明的更广泛的强制性异常,反之则可以。
构造方法不能被重写。
如果不能继承一个类,则不能重写该类的方法。
5.next()和nextLine()的区别
next():只读取输入直到空格。它不能读两个由空格或符号隔开的单词。此外,next()在读取输入后将光标放在同一行中。(next()只读空格之前的数据,并且光标指向本行)
nextLine():读取输入,包括单词之间的空格和除回车以外的所有符号(即。它读到行尾)。读取输入后,nextLine()将光标定位在下一行。
因此可以知道:如果是用空格间隔的东西,就用next就可以完美解决
除此之外,nextLine()由于会受回车影响,因此在使用的时候要吸收回车