一、数组
数组也是一种引用类型,其父类是Object,使用“数据类型[]”声明,如“int[] array”表示声明了一个元素类型为int类型的数组array。
数组初始化语法:
// 静态初始化语法,即定义的时候就初始化好了所有的元素
int[] array1 = {100, 55, 30};
// 动态初始化语法,初始化时只定义好数组中元素的个数,new int[5]表示创建一个
// 有5个int类型元素的数组,但是并没有初始化数组中元素的值,只是赋予了默认值,即
// 基本数据类型的默认值和引用类型的默认值null。
int[] array2 = new int[5];
使用数组时,应注意以下几点:
-
数组是一种容器,数组当中可以存储基本数据类型的数据,也可以存储引用数据类型的数据。对于基本数据类型,数组中存储的是数据的值,而引用类型,数组当中存储的是对象的引用,即内存地址。
-
数组因为是引用类型,所以数组对象是存储在堆内存当中的。
-
数组一旦创建,其长度是不可变的。
-
数组中的元素的数据类型是统一的,如int数组则表示此数组中的元素全部都是int类型的。
-
所有数组对象都有length属性(注意不是length方法),用来获取数组中元素的个数。
-
数组中在存储时,数组中的元素的内存地址都是连续的。
-
数组对象的内存地址是数组中第一个元素所在的内存地址。
-
使用下标访问数组时,如果下标超出了数组的长度,则会发生ArrayIndexOutOfBoundsException异常。
数组的访问和赋值:
直接通过下标进行访问和赋值即可,如“array1[0]=22;”。
数组的优点和缺点:
-
优点:
根据下标去检索元素时效率极高,因为数组中的元素在空间地址上是连续的,并且每个元素占用的内存空间是相同的,检索某个元素时只需要根据数组内存地址的起始位置就可以算出这个元素的内存地址,所以检索第100个元素和第100万个元素的地址的时间都是一样的。
-
缺点:
一个是,为了保证数组中每个元素的内存地址连续性,所以在数组中间的某个位置删除或增加元素时,会涉及到元素的向前或者向后位移的操作,此时的效率就会极低。另外一个是,数组不能存储大数据量,因为很难在内存空间上找到一块特别大的连续的内存空间。
数组扩容:
Java中数组扩容的原理或者说方法是将小容量的数组使用“System.arraycopy”方法拷贝到大容量的数组当中,然后删除小容量的数组,Java中数组的扩容效率是较低的,所以在数组的使用当中,应该尽量避免数组的拷贝和扩容。
二维数组:
二维数组,包括三位数组等多维数组,其实就是数组中的元素又是一个数组,多少维其实由这个数组的元素“套了多少层”决定的,二维就是数组中的元素是一个一维数组,同理,三位数组中的元素是一个二维数组,然后以此类推即可。
// 静态初始化语法
int[][] a = {
{1, 2, 3},
{4, 5, 6},
// 动态初始化语法
int[][] array = new int[3][4];
Arrays工具类:
这个工具类最常用的就是sort和binarySearch这两个方法,但是注意,二分查找方法的前提是数组已经排好序了。
import java.util.Arrays;
public class ArrayToolsTest{
public static void main(String[] args){
int[] myArray = {3, 2, 6, 4};
// sort方法可以将一个数组排序,但是注意,sort方法并没有返回值,
// 即不是返回排好序的数组,而是直接排序传入的数组
Arrays.sort(myArray);
// 二分查找算法的前提是需要数组已经排好序了
// 返回值为元素在数组中的下标,元素在数组中不存在则返回-1
// 但是这个方法多用来判断数组中有没有这个元素,因为如果数组中该元素
// 不只一个的话,那么返回的下标不一定是第一个元素的下标
int indexd = Arrays.binarySearch(myArray, 6);
}
二、异常
1、对异常的理解
异常也是类,每一个异常类都可以创建异常对象。
异常继承结构:
通过帮助文档中可以看到,java.lang.Object --> java.lang.Throwable,而Throwable下有两个子类Error和Exception,它们都是可以抛出的,对于这两个子类分支,Error分支下的子类(包括Error自身)称之为错误,错误一旦发生,通常是直接就退出程序了,没有办法及时去处理。而Exception分支下的子类(包括Exception自身)称之为异常,异常是可以在代码层面提前去处理的,Exception的子类又可以分为两个分支,一个分支是RuntimeException及其子类,称为运行时异常,另一个分支就是除RuntimeException外的其它Exception的直接子类,也称为编译时异常。
编译时异常:
之所以称之为编译时异常,是因为在编译阶段就可以发现并提醒程序员提前处理这种异常,对于这类异常怎么提前去处理,还是得要实际使用中多练才能有更深的体会。
运行时异常:
这类异常在编译时不会报错,但是编译通过之后在运行时又会出错,所以叫运行时异常,比如对于表达式10/0,除数为0肯定是错的,但是编译器并不会识别并提醒,编译通过之后运行的时候肯定就会报错了。
异常处理:
处理异常的方式有两种,一种是使用throws关键字和throw关键字,将异常抛出给上一级,让上一级去处理(上一级此时必须处理这个异常);另一种是使用“try...catch”语句把异常捕获,但是注意,捕获到了这个异常不一定要去处理它,让它直接“过”也是允许的。
2、throws抛出异常
throws使用示例:
public class ThrowsTest{
public static void main(String[] args){
// 这里在编译时会发生错误,也就是编译时异常,之所以有这个异常
// 因为在func定义中有throws关键字,表示这个方法在执行过程中可能会发生
// 对应的异常(ClassNotFoundException),所以它的上一级必须去处理
// 这个异常,不处理的话,编译器就会不通过。
func();
// 使用throws关键字抛出可能发生的异常
public static void func() throws ClassNotFoundException{
System.out.println("my func!!!");
// 使用throw手动抛出一个异常
throw new ClassNotFoundException("未找到类异常!");
}
关于throws的使用,需要注意:
-
throws抛出的异常,通常有两种处理方式,一种是继续使用throws关键字向上一级抛出同样的异常,即调用者自身不处理这个异常,让再上一级去处理。另一种是使用try...catch语句去捕获抛出的异常。
-
Java内置类或者我们自己定义的类如果有使用throws关键字,就表示它是编译时异常,使用的时候必须要去处理它。
-
throws抛异常时,既可以抛出具体的异常,也可以抛出它的某个父类异常,最顶级的异常类可以是Exception类,它包含了所有异常。
-
使用throws的时机就是如果这个异常希望调用者来处理,那么就是用throws抛出它,其他情况应该使用try捕获的方式。
3、try捕获异常
try使用示例:
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class ExceptionTest {
public static void main(String[] args) {
FileInputStream fis = null;
try {
// 将可能会发生异常的语句放在try的语句块中
// try语句块中的代码一定会去执行,直到发生异常为止
fis = new FileInputStream("Z:\\Study\\temp.md");
System.out.println(10 / 0);
} catch (ArithmeticException e) { // 捕获可能会发生的异常
// 捕获到异常后,对异常进行处理
// catch语句块中的代码只有捕获到异常之后才会执行
// ArithmeticException e这个语句相当于是声明一种引用类型的变量,类似于方法的形参,e保存的是异常对象的内存地址,并且可以在catch语句块中去使用这个对象引用e。
System.out.println("发生算术异常!!!");
} catch (FileNotFoundException e) { // 可以使用多个catch语句块捕获不同的异常
// 多个catch时会从上到下依次捕获,执行了一个catch之后,其他的catch就不会执行了
// 并且多个catch语句时,应该遵循从上到下的“从小到大”捕获,即具体异常在前,它的父类型依次往后
System.out.println("文件不存在!!!");
} finally {
// finally块中的语句无论是否发生异常都会处理,哪怕try块最后有个return语句
// 执行到return语句时也会先执行finally中的语句,再执行return
// 可以用来处理一些无论是否发生异常都要处理的操作,如关闭文件流等
if (fis != null){
fis.close();
} catch(IOException e) {
e.printStackTrace();
System.out.println("关闭文件等其他操作。。。");
}
注意,catch捕获的异常可以是具体的异常,也可以是具体异常的父类型异常,此时可以理解为多态。
异常对象中的常用方法:
-
getMessage():
获取异常的简单描述信息。
-
printStackTrace():
打印异常追踪的堆栈信息。
4、自定义异常
自定义的异常类需要继承Exception或者RuntimeException,并且需要定义无参和有参两个构造方法。
public class MyException extends Exception{
public MyException{
public MyException(String s){
super(s);
}
自定义异常中的方法重写或者覆盖时需要注意一个语法问题,重写的方法抛出的异常不能比父类的方法更大或者说更宽泛,只能更小或者说更具体,比如父类异常方法抛出了IOException异常,那么异常子类中重写这个方法时就不能抛出Exception异常,但是可以抛出IOException异常或者FileNotFoundException异常。
三、泛型
泛型在使用尖括号“<标识符1,标识符2,...>”来表示,标识符代表的是某种类型。
泛型的作用其实是用它定义了一个模板,定义时并没有写死数据的类型,当真正使用的时候可以根据需要传入自己的数据类型。
自定义泛型:
/*
自定一个泛型只需要在类名之后使用<标识符>即可
注意,此处的标识符是随意定义,就像变量名一样
public class MyGenericTest<T> {
public static void main(String[] args) {
// 定义的时候,传入的类型是什么,那么创建的对象使用的泛型类型就是什么类型
MyGenericTest<String> mgt = new MyGenericTest<>();
mgt.func("自定义泛型方法测试!");
如果想要使用泛型定义的类型,在方法参数中直接使用即可
public void func(T t){
System.out.println(t);
}
集合中泛型的应用:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class GenericTest {
public static void main(String[] args) {
// 指定集合中的元素类型为Pet,不能存储其它类型的元素
// 使用new的时候可以不用再传入类型了,可以自动推断,此时的表达式<>也称为钻石表达式
// 如果不指定泛型,也是可以的,默认就是Object类型
List<Pet> petList = new ArrayList<>();
Cat c = new Cat();
Dog d = new Dog();
petList.add(c);
petList.add(d);
// 迭代器的声明也需要加上泛型的定义
Iterator<Pet> it = petList.iterator();
while (it.hasNext()) {
// 原本next方法返回值的类型为Object,使用泛型之后返回的类型直接就是指定
// 的类型,不需要进行类型转换了。
Pet p = it.next();
p.play();
// 当然,如果要使用具体的子类对象的方法,还是需要转型之后才能调用
if (p instanceof Cat){
Cat myCat = (Cat)p;
myCat.sleep();
if (p instanceof Dog){
Dog myDog = (Dog)p;
myDog.bark();
输出结果:
宠物在玩耍!
猫咪在睡觉!
宠物在玩耍!
狗子在嚎叫!
class Pet {
public void play() {
System.out.println("宠物在玩耍!");
class Cat extends Pet {
public void sleep() {
System.out.println("猫咪在睡觉!");
class Dog extends Pet {
public void bark() {
System.out.println("狗子在嚎叫!");