Java基础
由于我是中途写的,所以我不知道标号是啥了,所以就先这么写吧
第二章
包
访问修饰符
面向对象三大基本
封装
继承
super
多态
好处:提高代码复用性,有利于代码的维护
多态是建立在封装和继承的基础之上的
方法的多态
重写和重载就体现了多态
对象的多态
- 一个对象的编译类型和运行类型可以不一致
1 | Animal animal =new Dog(); |
animal的编译类型是Animal,运行类型是Dog。将父类的引用指向子类的对象
编译类型在定义对象时就确定了,不能改变
运行类型是能改变的
1 | Animal animal =new Dog(); |
运行类型变为了Cat,编译类型仍然是Animal
- 编译类型看等号的左边,运行类型看等号的右边
案例1
1 | public class Animal { |
这时候先输出狗叫,后输出猫叫
案例2
实现主人给动物喂食
1 | //改进前 |
对于改进方法,由于可能有多个动物,如果每一个动物都编写一个方法,显然过于繁琐。因此可以采用多态的方法来改进,代码如下
1 | //将feed方法这么改 |
animal的编译类型是Animal,可以接受Animal子类的对象,food同理,因此在主方法里的dog和bone都可以传给animal和food
多态的注意事项
多态的前提是两个对象存在继承关系
向上转型,也叫做自动类型转换(父类的引用指向子类的对象)
可以调用父类的所有成员(属性和方法),但不能调用子类的特有成员。因为在编译阶段,能调用哪些成员是由编译类型来决定的
最终运行效果看子类的具体实现,即调用方法时,按照从子类开始查找方法,然后调用
向下转型,也叫做强制类型转换(将父类引用强制转换为子类引用)
只能强转父类的引用,不能强转父类的对象
1 | public class A { |
方法b不在编译类型A里,因此找不到方法b,报错
解决办法:向下转型(公式:子类类型 引用名 = (子类类型) 父类引用)
1 | A a=new B(); |
也许读到这你就会疑惑了,为什么我不能直接 B b=new B();
向下转型岂不是多此一举吗?
每一种方法的创造必然有它的价值,向下转型亦如此。其实这么做是为了泛型编程而考虑的。这里由于知识匮乏,暂时找不出例子以及实现原理,待后续补充
属性没有重写之说
属性的值看编译类型
instanceof
比较操作符java的保留关键字,测试左边的对象是否为右边的类或者其子类型,返回boolen数据类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public class test {
public static void main(String[] args) {
B b=new B();
System.out.println(b instanceof B);
System.out.println(b instanceof A);
System.out.println(b instanceof Object);
A a=new B();
System.out.println(a instanceof B);
System.out.println(a instanceof A);
System.out.println(a instanceof Object);
Object object=new Object();
System.out.println(object instanceof A);
}
}
class A{
}
class B extends A{
}这里空写了两个类,B继承A。对于b的编译类型属于A以及Object的子类型,前三个返回true。同理,中间三个也是返回true。对于最后一个,object的编译类型时Object不属于A的子类型,因此返回false
案例3
1 | Object obj=new Integer(9); |
运行该段代码会抛出异常java.lang.Integer cannot be cast to java.lang.String
什么意思呢,是指int类型不能强转为String类型,这里属于向下转型,但是Integar与String不在同一个对象阶层,强制转换只会在同一个对象阶层转化
比如这里的B和C不属于同一阶层不能强制转换,而C和D属于同一阶层,可以强制转换
动态绑定机制
当调用对象方法时,该方法会和该对象的内存地址(运行类型)绑定
当调用对象属性时,没有动态绑定机制,哪里声明,哪里使用
这两句话什么意思呢,下面看代码解读:
1 | public class A { |
首先,a的运行类型是B,第一句话中的a.sum在B中没有此类方法,因此要去父类中找,于是跳转到A中的sum方法,但A中的sum方法的返回值中含有getl方法。我们看,A和B类中都有getl方法,这里是用到了方法重写,那么接下来程序会跳转到那个类的getl方法呢?显然是B中的,上文说过:一个方法会和该对象的运行类型绑定,a的运行类型是B,于是就调用了B类的getl,而B类中getl的i是属性,在B中声明为20,就使用B中给的值
上述程序很好的体现了上文所说的两句话
多态的应用
- 多态数组
数组的定义类型为父类类型,里面保存的实际元素类型为子类类型
实现下列功能:创建Person、Student和Teacher对象,统一放在数组中,并调用say方法
1 | public class Person { |
其实,上述在调用say方法的时候启用了动态绑定,person[i]的编译类型为Person,而运行类型根据实际情况来看,new后面为什么就调用什么类里的方法
那么我们怎么调用子类中特有的方法呢,比如在Teacher里加一个teach方法,Student里加一个study方法
1 | public void teach(){ |
但是我们如果直接使用persons[i].teach
或者persons[i].study
,则会报错。我们看Person[] persons=new Person[5]
这句话代表persons[i]的运行类型为Person,而在Person类里没有teach和study方法,编译器找不到,因此报错。
解决办法:向下转型。
1 | if(persons[i] instanceof Student){ |
这里需要加一个判断,因为不知道persons的运行类型具体是什么,如果直接盲目强转会抛出异常。此外,对于Person的判断语句是一个空语句,没有实际操作。
- 多态参数
下面来看一个案例,能够很好地体现多态参数的含义
定义员工,普通员工和经理类。员工为父类,包含姓名和月工资,以及计算年工资的方法,经理类多了奖金属性和管理方法,普通员工类多了工作方法。
此外还有一个测试类,添加获取年工资的方法和判断员工种类的方法。
1 | public class Employee { |
这里注意,testwork和show方法内涵不同,前者是判断传入的实参到底为何种类型,进行向下转型,而后者为动态绑定机制,getAnnual取决于传入的实参的运行类型。
Object类的几个简单方法
Equals
Equals简介
equals方法是Object超类中的一个方法,默认情况下,比较内存地址是否相等
1 | //定义一个A类 |
如上,new了两个A类对象,因此地址元素不同,故返回false
如果将第二行语句改为A b=a;
这时候返回true,两者内存地址相等
实际情况下,一般会重写equals方法,如String类的equals方法比较的是两个对象的内容,而不是内存地址
1 | String a=new String("aaa"); |
虽然new了两个String对象,但是由于String类的equals方法被重写,因此比较内容,返回true
Equals与==的区别
==
主要用于基本类型之间的比较(int,char,boolen……)当用于对象之间的比较是,主要以地址是否相等为判断标准
equals
主要用于对象之间的比较,可以通过重写equals方法来比较内容是否相同
值得注意的是,基本数据类型不能使用equals用来判断
hashCode
返回该对象的哈希码值,提高哈希表的性能,这里仅做一个简单的介绍
两个引用如果指向的是同一个对象,则哈希值肯定是一样的
两个引用如果指向的是不同对象,则哈希值是不一样的
哈希值主要根据地址号来的,不能将哈希值等价于地址
toString
返回类名@哈希值的十六进制
finalize
1 | A a=new A(); |
这里的a对象就是一个垃圾对象,垃圾回收器就会回收(销毁)对象,将其空间释放,供别的对象使用。
当然,也可以重写finalize方法,实现自己的逻辑,举个简单的重写例子
1 | public class Finalize { |
我们在重写的finalize方法里写了两句话,当对象被销毁时,则会输出这两句话,而当我们运行代码的时候发现它并没有输出,这是怎么回事呢?跟算法有关,并不是说某个对象一变成垃圾就会被回收。有没有某种方式能够直接体现这种销毁的思想呢,当然有,在主方法里加一句System.gc();
这句话表示运行垃圾回收器
运行结果:程序结束
销毁
释放……
这里肯定又有人疑惑了,明明System.gc()代码在结束先,不是应该先输入finalize方法里的语句吗?关于这个疑惑,咱后续再讲
在实际开发中,几乎用不到finalize,更多的是为了应付面试
断点
第三章(面向对象高级部分)
类变量和类方法
类变量
定义
类变量也叫静态变量, 定义在类中函数体之外的变量,可以被该类的所有对象实例共享,有static修饰。不加static修饰的称为实例变量/普通变量/非静态变量/非静态成员变量
1 | public class test01 { |
如上,a和b均为A类的实例,而sum为类变量,a和b共享,因此a.sum++;
和
1 | b.sum++;`语句均使用一个sum,输出结果为`2 2 |
内存
类变量在类加载的时候就生成了
(未完待续)
访问
对于上面的例子,还可以通过A.sum
来访问类变量,即使没有创建对象实例。前提是满足访问修饰符的访问权限和范围。
细节
- 实例对象不能通过
类名.变量名
访问 - 只要类加载了就能用类变量了
- 类变量的生命周期是随类的加载开始,随类的消亡而销毁
类方法
类方法的定义跟前面类变量定义同理
具体使用场景
当方法中不涉及到任何和对象相关的成员,则可以将方法设计成静态方法,提高开发效率。不创建实例也可以调用某个方法,如开发自己的工具类时,可以将方法做成静态的,方便调用
细节
- 类方法中不允许使用和对象有关的关键字,比如
this
和super
- 类方法只能访问静态变量或静态成员
- 普通成员方法,既可以访问非静态成员,也可以访问静态成员
main方法语法
- main方法是java虚拟机调用的,所以该方法必须得是public修饰
- java虚拟机在执行main方法时无需创建对象,所以该方法必须是static
- main方法接收string类型的数组参数,该数组保存执行java命令时传递给所运行的类的参数
例如,我们可以通过下列方式来遍历该数组
1 | public class A { |
- 在main方法中,我们可以直接调用main方法所在类的静态属性和静态方法,但是不能直接访问该类中的非静态成员,必须创建该类的一个实例对象,才能通过这个对象去访问该类的非静态成员
代码块
定义
代码块又称为初始化块,属于类中的成员,类似方法,将逻辑语句封装在方法体中,通过{ }包围起来
但和方法不同,没有方法名,没有返回,没有参数,只有方法体,而且不用通过对象或类显式调用,而是加载类时,或创建对象时隐式调用。
语法
1 | [修饰符]{ |
代码
1 | }; |
- 修饰符可写可不写,写的话只能写
static
- 有
static
修饰的叫静态代码块,否则叫作普通(非静态)代码块 - 分号可写可不写
好处
相当于对构造器的补充,用来初始化的操作,如果多个构造器中都有重复的语句,可以加到初始化块中,提高代码的复用性
使用方法
把相同的语句放在一个代码块中,这样当我们调用构造器的时候,创建对象都会先调用代码块的内容
注意事项
- 静态代码块只会执行一次,而普通代码块每创建一个对象就执行一次
- 类在什么时候被加载
1.创建对象实例
2.创建子类对象实例,父类会被加载
1 | public class AA { |
比如这里BB为AA 的子类, 两个类里均有代码块,当main方法里创建BB类对象时,会先输出父类代码块里的东西后输出子类代码块的东西
3.使用类的静态成员
1 | public class CC { |
值得注意的是,如果我们把上面代码块前面的static
去掉,则不会输出代码块里面的内容。只是使用类的静态成员,代码块不会被执行
- 创建一个对象时,在一个类中的调用顺序
1.调用静态代码块和静态属性初始化时,二者优先级相同,如果有多个静态代码块和多个静态属性初始化,则按照它们定义的顺序调用
1 | public class A { |
上面是先调用静态代码块,再调用静态属性初始化,则输出结果为: 代码块
getN1被调用
如果将二者调换成如下顺序,那么输出结果将颠倒
1 | private static int n1=getN1(); |
2.调用普通代码块和普通属性初始化时,同上,顺序依旧取决于定义的先后
3.当普通与静态混合使用时,静态代码块与属性初始化永远比普通代码块与属性要先执行,然后各自依照定义的顺序执行。此外, 构造器的优先级是最低的。现在我们补充一下上面的实例
1 | class AA{ |
可以看到新增了普通代码块和普通属性初始化,还有构造器。但运行后发现顺序正好相反。先输出“静态代码块”和“getN1被调用”,然后再输出“普通代码块”和“getN2被调用”,最后才输出“构造器被调用”。上面这段代码很好的诠释了三者之间的顺序。
那么在继承关系中,它们的顺序又是什么呢?那么就引出我们即将要说的内容了
- 创建一个子类对象时,在父类与子类中的调用顺序
还是老样子,先找静态,静态执行完之后再回去找普通。话不多说,上石山代码
1 | public class test { |
B类继承A,当我们在main方法中创建B类对象以后,调用顺序如下
- 父类的静态代码块和静态属性初始化
- 子类的静态代码块和静态属性初始化
- 父类的普通代码块和普通属性初始化
- 父类的构造方法
- 子类的普通代码块和普通属性初始化
- 子类的构造方法
一句话总结一下,先找静态,再找普通,每一次寻找的顺序都是由父类往子类找
- 类处于继承关系时的顺序
前面的知识中提到,创建一个类时,默认隐藏了super
语句,如果父类中有代码块则先输出父类的代码块,这是容易忽略的一点,面试的高频考点
1 | public class test { |
显而易见,输出结果为
1 | AA代码块 |
使用场景
设计模式之【单例设计模式】
采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。下面我们来介绍两种简单的模式
- 饿汉式
1.构造器私有化
2.类的内部创建对象
3.向外暴露一个静态的公共方法
现在我们用代码实现该功能:一个人同时最多只能有一个女朋友
1 | public class singleTest01 { |
由于我们在构造器前面加上了private,在主方法new是行不通的,这也就避免了我们创建很多新对象。然后我们在此类里创建了一个新对象,但此时我们还不能在主方法里直接调用,需要用到另一个公共static方法:getInstance,返回该对象,这样我们就能在主方法里调用了。要注意这里的个体Instance方法的返回类型是类名。
但是,饿汉式还有个缺点,无论你有没有用到这个实例对象,饿汉式都会创建,这时候可能会造成资源浪费,下面我们的懒汉式就能很好解决这个问题了
- 懒汉式
1 | public class singleTest01 { |
还是上面的案例,这里我们新创建了一个静态属性,众所周知当使用静态属性的时候默认加载这个类,但结果并没有“输出构造器被调用”
以及“getInstance被调用“
,这说明懒汉式并不自动创建实例对象,只有使用getInstance
方法才会返回该对象,再次调用时,会返回上次创建的对象。不会造成资源的浪费
- 区别
1.二者最主要的区别就是在于创建对象的时机不同:饿汉式是在类加载就创建了对象实例,而懒汉式实在使用时才创建
2.饿汉式不存在线程安全问题,懒汉式存在线程安全问题
final
使用情况
- 不希望类被修饰,可以用final修饰
- 不希望父类的某个方法被子类覆盖/重写,可以用final修饰
- 不希望类的某个属性的值被修改,可以用final修饰
- 当不希望某个局部变量被修改,可以用final修饰
细节
- final修饰的属性又叫常量
- final修饰的属性在定义时,必须赋初值,且不能再修改,赋值可以在如下位置之一
1.定义时
2.在构造器中
3.在代码块中
- 如果final修饰的属性是静态的,则初始化的位置只能是定义和静态代码块中,不能在构造器中赋值
- final类不能被继承,但可以实例化对象
- 如果类不是final类,但是含有final方法,此方法虽然不能重写,但可以继承
- 一般来说如果一个类已经是final类了,不需要再将其方法用final修饰
- final不能修饰构造器
- final和static往往搭配使用,效率更高,不会导致类加载
1 | public class test01 { |
一般来说当调用A类静态属性会加载整个类,但这里我们将属性用final修饰,可以做到仅仅调用这个属性而不加载类,从而提高效率
抽象类
所谓的抽象方法就是没有实现的方法,没有方法体。当一个类中存在抽象方法时,需要将该类声明为抽象类。一般来说,抽象类会被继承,由其子类来实现抽象方法
细节
- 语法:
abstract 方法名
注意后面不能加花括号,没有方法体 - 抽象类的价值更多是设计,设计者设计好后,让子类实现,在框架用的较多
- 抽象类不能被实例化
- 抽象类可以有任意成员,比如非抽象方法、构造器、静态属性等等,不一定要包含abstract方法
1 | abstract class A{ |
abstract
只能修饰类和方法,不能修饰属性或其他的- 如果一个类继承了抽象类,则它必须实现抽象类的所有抽象方法,否则将自己声明为abstract类
- 抽象方法不能使用private、final和static来修饰,因为这些关键字都是和重写相违背的
实践-设计模式之【模板设计模式】
假设现在我们要实现这么一个方法,有很多类,每个类包含几个方法,统计这些方法运行的时间
基础版
1 | public class A { |
这里我们就写了一个类,其他的类同理。但我们会发现一个问题,倘若要写很多类,每一个类的方法中我们都要写一个统计时间的代码,那将很耗费时间。能不能改进呢?当然,就用到了所谓的“模板设计模式”
终极版
1 | public abstract class tamplate { |
这里我们将计算时间集成为一个方法,放在抽象类tamplate中,此外还有一个抽象方法job。当我们创建一个新类并继承tamplate时,需要对job方法重写,这样我们就不需要在每个方法体中编写计算时间的代码了,大大降低了繁琐程度
注意,这里运用到了动态绑定机制,当我们在主方法中调用time时,会先从子类中找有没有这个方法,然后在父类中寻找,当运行到time方法中的job时,会与运行类型绑定,自然就跳转到该运行类型的job方法中去了
接口
介绍
接口就是给出一些没有实现的方法,封装到一起,到某个类要使用的时候,再根据具体情况把这些方法写出来
1 | interface itf{ |
注意:在jdk7以前,接口里的所有方法都没有方法体,即都是抽象方法;而在jdk8以后接口类可以有静态方法,默认方法,也就是说接口中可以有方法的具体实现
枚举和注解
枚举类
按照传统思路去写只拥有几个固定属性的对象(如季节等)是不适合的,因为能随意修改。我们引入了枚举类。
枚举是一组常量的集合,是一种特殊的类,里面只包含一组有限的特定的对象。
实现方式
自定义类实现枚举
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29public class EnumerationClass01 {
public static void main(String[] args) {
System.out.println(season.SPRING.getName()+" "+season.SPRING.getDesc());
System.out.println(season.SUMMER.getName()+" "+season.SUMMER.getDesc());
System.out.println(season.AUTUMN.getName()+" "+season.AUTUMN.getDesc());
System.out.println(season.WINTER.getName()+" "+season.WINTER.getDesc());
}
}
class season{
//构造器私有化,防止在外部直接新建(new)
//去掉set(只读)防止属性被修改
private String name;
private String desc;
//优化,加入final修饰
public final static season SPRING =new season("春天","温暖");
public final static season SUMMER =new season("夏天","炎热");
public final static season AUTUMN =new season("秋天","凉爽");
public final static season WINTER =new season("冬天","寒冷");
private season(String name,String desc){
this.desc=desc;
this.name=name;
}
public String getName(){
return name;
}
public String getDesc(){
return desc;
}
}为了防止随意修改属性,我们将原本的
setXXX
方法删去,只提供getXXX
方法,即只读。通常对枚举对象/属性使用final+static修饰(对外暴露对象),以实现底层优化。枚举对象名通常全部使用大写,常量的命名规范。enum
关键字1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18enum season2{
SPRING("春天","温暖"),
SUMMER("夏天","炎热"),
AUTUMN("秋天","凉爽 "),
WINTER("冬天","寒冷");//写在前面,多个对象要用用逗号隔开
private String name;
private String desc;
private season2(String name,String desc){
this.desc=desc;
this.name=name;
}
public String getName(){
return name;
}
public String getDesc(){
return desc;
}
}基本语法与上面略有不同,不过本质上都是类似的。值得注意的是,在这里 如果有多个枚举对象,需要用逗号隔开。还有就是枚举对象要写在最前面。
当我们通过
javap
反编译class文件时,会发现我们用enum
写的枚举类默认继承Enum
类,而且是一个final类。

如果使用无参构造器创建枚举对象,则实参列表和小括号都可以省略
1 | DAY();//DAY; |
enum常用方法
这里我们还是用上面season2
这个枚举类。
name()
输出枚举对象的名字。
1
2season2 spring=season2.SPRING;//SPRING
System.out.println(spring.name());ordinal()
输出该枚举对象的次序,第一个为0,第二个为1,以此类推……
1
System.out.println(spring.ordinal());//0
由于我们的spring是第一个枚举对象,因此输出0.同理如果后面跟着
summer
,则输出1.values()
返回含有所有定义的枚举对象的数组。
1
2
3
4season2 v[]=spring.values();
for(int i=0;i<4;i++){
System.out.println(v[i]);
}这里的v就包含了四个季节。
valueOf()
根据输入的字符串再所有枚举对象中查找,如果找到了就返回,没有找到就报错。
1
2season2 spring2=season2.valueOf("SPRING");
System.out.println(spring==spring2);//true注意这里的
pring2
和上文提到的spring是一个东西,当我们判断它们是否相等时,输出true
,可以验证。如果我们将后面的”SPRING
“改为”SPING
“那么系统找不到,就报错。compareTo()
把两个对象的序号比较,返回的结果为前面的序号与后面的序号之差。
1
System.out.println(season2.SPRING.compareTo(season2.AUTUMN));
SPRING
的序号为0,AUTUMN
的序号为2,因此输出结果为-2.
enum
实现接口
使用
enum
关键字就不能继承其他的类了,因为enum
隐式继承了Enum
,而Java是单继承机制枚举类实现接口如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14public class EnumExercise02 {
public static void main(String[] args) {
A.MEIJU.m();
}
}
enum A implements AA{
MEIJU;
public void m() {
System.out.println("m");
}
}
interface AA{
public void m();
}注解
注解也被称为元数据,用于修饰解释包、类、方法、属性、构造器和局部变量等数据信息。
基本介绍
使用Annotation时要在其前面增加@符号,并把该Annotation当成一个修饰符使用,用于修饰它支持的程序元素。
下面介绍三个基本的Annotation:
@Override
限定某个方法,重写父类方法,该注解只能用于方法。如果写了
@Override
注解,编译器就会去检查该方法是否真的重写了父类的方法,如果重写了,编译通过,否则编译错误。1
2
3
4
5
6
7
8
9class A{
public void m1(){};
}
class B extends A{
.lang.Override
public void m1(){
System.out.println("m1");
}
}@Override
只能修饰方法,不能修饰其他类、包、属性等。@Deprecated
用于表示某个程序元素(类,方法等)已经过时。可以修饰方法、包、类、字段、参数等等。
1
2
3
4
5
6
7
8.lang.Deprecated
class A1{//过时不是不能用,只是不推荐使用
.lang.Deprecated
public int n1=1;
.lang.Deprecated
public void m(){
}
}可以做版本升级过渡使用。
@SuppressWarnings
当我们不希望看到警告的时候,可以用
@SuppressWarnings
来抑制警告信息。在{“ “}中,可以写入你希望抑制的警告信息。
1
@SuppressWarnings
抑制的范围和你放置的位置相关。
元注解
元注解为修饰注解的注解。
元注解主要分为
@Retention
只能修饰注解定义,用于指定该注解可以保留多长时间。
@Target
用于修饰注解定义,用于指定被修饰的注解能用于哪些程序元素。
@Document
用于指定被改元注解修饰的注解类将被
javadoc
工具提取成文档,在文档中显示。@Inherited
被他修饰的注解将具有继承性。如果某个类使用了被@Inherited修饰的注解,则其子类将自动具有该注解。
异常
基本概念
Java语言中,将程序执行中发生的不正常情况称为“异常”,但开发过程中的语法错误和逻辑错误不是异常。
分类
Error
:Java虚拟机无法解决的严重问题。比如系统内部错误,资源耗尽(栈溢出,内存不足等等),Error是严重错误,程序会崩溃Exception
:其他因编程错误或偶然的外在因素导致的一般性问题,可以使用针对性的代码进行处理。分为两大类:- 运行时异常(程序运行时,发生的异常)程序员应该避免其出现这样的异常,编译器检查不出来,这类异常很普遍,若全处理可能会对程序的可读性和运行效率产生影响。
- 编译时异常(编程时,编译器检查出的异常)编译器要求必须处置的异常。
常见的运行时异常
NullPointException
空指针异常: 当应用程序试图在需要对象的地方使用null时,抛出该异常。
1
2
3
4
5
6public class NullPointException {
public static void main(String[] args) {
String name=null;
System.out.println(name.length());
}
} 这里我们输出了一个空字符串的长度,显然是错误的,就会抛出
Exception in thread "main" java.lang.NullPointerException
的异常。ArithmeticException
数学运算异常: 当出现异常的运算条件时,比如一个数除以0等等,就会抛出该异常。
ArrayIndexOutOfBoundsException
数组越界异常:1
2
3
4
5
6public class ArrayIndexOutOfBoundsException {
public static void main(String[] args) {
int[]a=new int[4];
System.out.println(a[6]);
}
}抛出
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException
ClassCastException
类型转换异常: 当试图将对象强制转换为一个不是实例的子类时,抛出该异常。
1
2
3
4
5
6
7
8
9
10public class ClassCastException {
public static void main(String[] args) {
A b=new B();
B b1=(B)b;
C b2=(C)b;
}
}
class A{}
class B extends A{}
class C extends A{} 主方法中第二条语句的向下转型是对的,因为b的运行类型本来就是B.而第三条语句中的向下转型明显就是两个无关的类(B和C)故抛出异常。
NumberFormatException
数字格式不正确异常 当应用程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常。
1
2
3
4
5
6public class NumberFormatException {
public static void main(String[] args) {
String name="aaa";
int num=Integer.parseInt(name);
}
}这里我们不能将一个字母类型的字符串转换成整数类型,因此抛出异常。
异常处理
异常处理的方式
try-catch-finally
如果认为一段代码有异常抛出,可以用以下代码捕获异常。系统将异常封装成Exception对象e,传递给catch.
1
2
3
4
5
6
7
8try{
...
}catch(Exception e){
//捕获到异常
}finally{
//不管try代码块是否有异常发生,始终要执行finally
//主要用来资源的关闭
} 如果没有发生异常,catch代码块不执行,但是finally仍然执行。如果没有finally也可以。
如果发生了异常,则异常后面的代码不会执行,直接进入到catch块,如果有finally,最后还需要执行finally里面的语句。比如下面的例子,在
Integer.parseInt(name)
处遇到了异常,那么我们就直接进入catch,输出异常,而不会执行输出name的语句。1
2
3
4
5
6
7try {
String name="aaa";
int num=Integer.parseInt(name);
System.out.println(name);
} catch (NumberFormatException e) {
System.out.println(e);
}throws
如果一个方法中的语句执行时可能生成某种异常,但不能确定怎么处理这种异常,则应显示地声明抛出异常,表明该方法将不对这些异常进行处理,而由该方法的调用者负责处理。
1
2
3public void f1() throws FileNotFoundException {//文件不存在异常
FileInputStream fis=new FileInputStream("...");
}//让调用f1方法的调用者处理异常有以下两点值得注意
- throws后面的异常类型可以是方法中产生的异常类型,也可以是它的父类;
- throws关键字后面也可以是异常列表,即可以抛出多个异常。
遇到异常可以抛给上一级,或者使用try-catch-finally机制。值得注意的是,try-catch-finally机制和throws机制只能二选一,不能两个都用。直到遇到
JVM
(最顶级)它处理异常的机制为输出异常信息,退出程序。如果一段代码既没有用try-catch-finally机制也没有用throws机制,那么系统默认使用throws.
使用细节
对于编译异常,程序中必须处理,比如try-catch或throws
1
2
3
4
5
6
7
8class AA{
public static void m1() throws FileNotFoundException{
FileInputStream fis=new FileInputStream("...");
}
public void m2(){//这么写报错,必须得在后面也加上throws FileNotFoundException或try-catch
m1();
}
}m1抛出了一个编译异常,当m2调用时,m2不能不处理从m1接受的编译异常,因此需要使用try-catch或throws.
对于运行时异常,程序中如果没有处理,默认就是throws方式处理
1
2
3
4
5
6
7class AA{
public static void m1() throws ArithmeticException{
}
public void m2(){
m1();
}
}依旧是上面的例子,在这里ArithmeticException是运行时异常,程序默认以throws方式处理,故不会报错
子类重写父类的方法时,对抛出异常的规定:子类重写的方法,所抛出异常类型要么和父类抛出的异常一致,要么为父类抛出异常的子类型
1
2
3
4
5
6
7
8class Father{
public void m1()throws RuntimeException{
}
}
class Son extends Father{
public void m1()throws ArithmeticException{
}
}
上面的RuntimeException
为ArithmeticException
的父类,因此可以重写
- 在throws过程中,如果有方法try-catch,就相当于处理异常,就不需要使用throws了
自定义异常
当程序中出现了某些错误时,但该错误信息并没有在Throwable
中表述处理,这个时候可以自己设计异常累,用于表示该错误信息。
我们直接看例子:
1 | public class test { |
我们要实现一个判断年龄的功能,如果年龄小于十八岁,那么抛出一个异常。这里我们写了一个类AgeException
,它是继承了RuntimeException
的一个类。我们在自定义异常的时候,如果继承Exception,属于编译异常;如果继承RuntimeException
,属于运行异常。当需要抛出异常的时候,构造器将字符串传给父类,以此实现相关功能。
一般情况下,我们自定义异常是继承RuntimeException
,把自定义异常写成运行时异常的好处是可以使用默认的处理机制。
throws和throw的区别
意义 | 位置 | 后面跟的东西 | |
---|---|---|---|
throws | 异常处理的一种方式 | 方法声明处 | 异常类型 |
throw | 手动生成异常对象的关键字 | 方法体中 | 异常对象 |
常用类
包装类
分类
针对八种基本数据类型相应的引用类型
数据类型 | 包装类 |
---|---|
boolean | Boolean |
char | Character |
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
装箱和拆箱
首先先解释一下什么是装箱和拆箱。装箱就是基本类型转换为包装类型,而拆箱就是包装类型转换为基本类型。
jdk5以前是手动装箱和拆箱
1
2
3
4
5
6
7int i=100;
//手动装箱第一种写法
Integer integer=new Integer(i);
//第二种写法
Integer integer1=Integer.valueOf(i);
//手动拆箱
int i1=integer.intValue();jdk5以后是自动装箱和拆箱
1
2
3
4//自动装箱
Integer integer2=i;
//自动拆箱
int i2=integer2;
其实,自动装箱底层仍然使用的是手动装箱的方法,系统自动帮你写好了。
其他包装类的用法类似,这里就不过多赘述了。
小练习
1 | Object obj=true?new Integer(1):new Double(2.0); |
分析如上语句的输出结果。
三目运算符,前面为真,返回冒号前面的值,因此Object obj=new Integer(1)
,按理说应该输出1,但是这里的三目运算符要当成一个整体,因此要输出1.0
包装类方法
1 | //Integer和String互相转换 |
Integer类
我们来看几个样例
1 | Integer i1=new Integer(1); |
显而易见,即便是两个值均为1,但他们是新创建的两个对象,因此不等。
1 | Integer j1=1; |
这里很容易就想到j1=j2
,但真的是这样吗?上面我们提到了自动装箱的实质:Integer.valueOf
.我们来看一下这个方法的源码:
1 | public static Integer valueOf(int i) { |
可以看到这里有一个限制条件,传入的参数必须在一个范围之内,否则会创建一个对象。因此这里判断j1
和j2
是否相等就看传入参数的范围。IntegerCache.low
和IntegerCache.high
到底表示什么呢?我们再看它的源码可知,最小值为-127,最大值为128.因此,如果传入的参数小于等于128且大于等于-127,都不会创建新的对象。故这里j1
和j2
相等。
1 | Integer k1=128; |
所以这一段代码就不相等了。
1 | Integer i1=128; |
只要有基本数据类型,直接判断值是否相等,因此上面输出true.
String类
概述
字符串的字符使用Unicode字符编码,一个字符无论是字母还是汉字都占两个字节。
String类实现了接口
Serializable
(String可以串行化:可以在网络传输)、接口Comparable
(String对象可以比较大小)String是final类,不能被其他类继承
我们在翻String的源码时,会有这样一句话
private final char value[];
表示用于存放字符串内容,此外这个value被final修饰,故不可以修改。这里的修改不是指字符串的内容不能修改,而是value不能指向新的地址。
1
2
3
4final char v[] ={'A','B'};
v[0]='a';
char v2[]={'C','D'};
v=v2;//errorv字符数组我们用final修饰,我们将他指向v2直接报错。
创建
直接赋值
String s="xxx";
先从常量池查看是否有
"xxx"
数据空间,如果有,直接指向;如果没有,则重新创建,然后指向。s最终指向的是常量池的空间地址。调用构造器
String s=new String("xxx");
先在堆中创建空间,里面维护了
value
的属性,指向常量池的"xxx"
空间。如果常量池没有"xxx"
,重新创建,如果有,直接通过value
指向,最终指向的是堆中的空间地址。
内存
常用方法
1 | public static void main(String[] args) { |
StringBuffer
结构
StringBuffer
代表可变的字符序列,可以对字符串内容进行增删,很多方法与String是相同的,但StringBuffer
是可变长度的。
StringBuffer
的直接父类是AbstractStringBuffer
StringBuffer
实现了Serializable
,即它的对象可以串行化- 在
AbstractStringBuffer
中,有属性char[ ]value,但它不是final修饰的。该value数组存放字符串内容,存放在堆中 StringBuffer
是一个final类,不能被继承
String和StringBuffer
的对比
- String保存的是字符串常量,里面的值不能更改,每次String类的更新实际上就是更改地址,效率较低。
StringBuffer
保存的是字符串变量,里面的值可以更改,每次StringBuffer
的更新实际上可以更新内容,不用每次更新地址,效率较高。
构造
1、构造一个不带字符的字符串缓冲区,其初始容量为16个字符。我们进到StringBuffer的源码可以看到它的构造器:
1 | StringBuffer stringBuffer=new StringBuffer(); |
1 | public StringBuffer() { |
故容量(capacity)为16.
2、构造一个不带字符,但具有指定初始容量的字符串缓冲区,即对char[]的大小进行指定。同样我们可以看它的源码:
1 | StringBuffer stringBuffer1=new StringBuffer(10); |
1 | public StringBuffer(int capacity) { |
3、构造一个字符串缓冲区,并将其内容初始化为指定的字符串内容
1 | StringBuffer stringBuffer2=new StringBuffer("tom"); |
1 | public StringBuffer(String str) { |
转换
String–>StringBuffer
方式1:使用构造器
1
2String s="hello";
StringBuffer b1=new StringBuffer(s);方式2:使用append方法
1
2StringBuffer b1=new StringBuffer();
b1.append(s);StringBuffer–>String
方式1:使用
toString
方法1
String s2=b1.toString();
方式2:使用构造器
1
String s3=new String(b1);
方法
1 | public static void main(String[] args) { |
StringBuilder
StringBuilder
和StringBuffer
均代表可变的字符序列,方法是一样的。只不过StringBuilder
用于单线程,StringBuffer
用于多线程。
StringBuilder
实现了Serializable
,即它的对象可以串行化StringBuilder
是一个final类,不能被继承StringBuilder
对象字符序列仍然是存放在其父类AbstractStringBuffer
,的char[]value中,因此也存放在堆中StringBuilder
所有方法没有做互斥的处理,因此在单线程的情况下使用
String、StringBuffer和StringBuilder的比较
String
:不可变字符序列,效率低,但复用率高StringBuffer
:可变字符序列,效率高,线程安全StringBuilder
:可变字符序列,效率最高,线程不安全
如果字符串存在大量的修改操作,并且在单线程的情况下,使用StringBuilder
;如果在多线程的情况下,使用StringBuffer
;如果字符串很少修改,被多个对象引用,使用String
.
Math类
1 | //abs 绝对值 |
Arrays类
Arrays里面包含了一系列静态方法,用于管理或操作数组。下面具体介绍几种方法。
下面所有代码一定不能忘记导入java.util.Arrays
toString
1
2Integer[] a ={1,2,3,4,5};
System.out.println(Arrays.toString(a));遍历数组
sort
1
Arrays.sort(a);
sort排序后,会直接影响到实参。
此外也可以通过一个接口Comparator实现定制排序
1
2
3
4
5
6
7
8Arrays.sort(a, new Comparator<Integer>() {
public int compare(Integer o1, Integer o2) {
return o1-o2;
//源码最终执行到binarysort
//o1-o2还是o2-o1直接决定的返回的正负性,从而决定了排序顺序,即从大到小还是从小到大
}//实现Comparator接口的匿名内部类
});根据指定比较器产生的顺序对指定的对象数组进行排序,这里的Comparator就是比较器。下面来看一个具体的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25public class bubble_sort {
public static void main(String[] args) {
Integer a[]={5,2,1,4,3};
bubbleSort(a, new Comparator() {
public int compare(Object o1, Object o2) {
int i1=(Integer)o1;
int i2=(Integer)o2;
return i2-i1;
}
});//小括号不能漏
System.out.println(Arrays.toString(a));
}
public static void bubbleSort(Integer a[],Comparator c){
int i,j;
for(i=0;i<a.length;i++){
for(j=i+1;j<a.length;j++){
if(c.compare(a[i],a[j])>0){
int temp=a[j];
a[j]=a[i];
a[i]=temp;
}
}
}
}
}首先看到bubblesort里面的结构非常熟悉,这就是我们之前提到的匿名内部类,尤以结尾的小括号瞩目。而冒泡排序的关键在于两趟循环指向值的大小比较,因此我们在这里“设限”,将比较器塞进去,即compare方法,这样我们才能根据上面指定的正负性来合理调用if判断。
binarySearch