静态内部类

前面我们介绍了成员内部类,它就像成员变量和成员方法一样,是属于对象的,同样的,静态内部类就像静态方法和静态变量一样,是属于类的,我们可以直接创建使用。

java 复制代码
public class Test {
    private final String name;

    public Test(String name){
        this.name = name;
    }

    public static class Inner {
        public void test(){
            System.out.println("我是静态内部类!");
        }
    }
}

不需要依附任何对象,我们可以直接创建静态内部类的对象:

java 复制代码
public static void main(String[] args) {
    Test.Inner inner = new Test.Inner();   //静态内部类的类名同样是之前的格式,但是可以直接new了
  	inner.test();
}

静态内部类由于是静态的,所以相对外部来说,整个内部类中都处于静态上下文(注意只是相当于外部来说)是无法访问到外部类的非静态内容的:

image-20220924124919135

只不过受影响的只是外部内容的使用,内部倒是不受影响,还是跟普通的类一样:

java 复制代码
public static class Inner {

    String name;
    public void test(){
        System.out.println("我是静态内部类:"+name);
    }
}

其实也很容易想通,因为静态内部类是属于外部类的,不依附任何对象,那么我要是直接访问外部类的非静态属性,那到底访问哪个对象的呢?这样肯定是说不通的。

局部内部类

局部内部类就像局部变量一样,可以在方法中定义。

java 复制代码
public class Test {
    private final String name;

    public Test(String name){
        this.name = name;
    }

    public void hello(){
        class Inner {    //直接在方法中创建局部内部类
            
        }
    }
}

既然是在方法中声明的类,那作用范围也就只能在方法中了:

java 复制代码
public class Test {
    public void hello(){
        class Inner{   //局部内部类跟局部变量一样,先声明后使用
            public void test(){
                System.out.println("我是局部内部类");
            }
        }
        
        Inner inner = new Inner();   //局部内部类直接使用类名就行
        inner.test();
    }
}

只不过这种局部内部类的形式,使用频率很低,基本上不会用到,所以说了解就行了。

匿名内部类

匿名内部类是我们使用频率非常高的一种内部类,它是局部内部类的简化版。

还记得我们在之前学习的抽象类和接口吗?在抽象类和接口中都会含有某些抽象方法需要子类去实现,我们当时已经很明确地说了不能直接通过new的方式去创建一个抽象类或是接口对象,但是我们可以使用匿名内部类。

java 复制代码
public abstract class Student {
    public abstract void test();
}

正常情况下,要创建一个抽象类的实例对象,只能对其进行继承,先实现未实现的方法,然后创建子类对象。

而我们可以在方法中使用匿名内部类,将其中的抽象方法实现,并直接创建实例对象:

java 复制代码
public static void main(String[] args) {
    Student student = new Student() {   //在new的时候,后面加上花括号,把未实现的方法实现了
        @Override
        public void test() {
            System.out.println("我是匿名内部类的实现!");
        }
    };
    student.test();
}

此时这里创建出来的Student对象,就是一个已经实现了抽象方法的对象,这个抽象类直接就定义好了,甚至连名字都没有,就可以直接就创出对象。

匿名内部类中同样可以使用类中的属性(因为它本质上就相当于是对应类型的子类)所以说:

java 复制代码
Student student = new Student() {
    int a;   //因为本质上就相当于是子类,所以说子类定义一些子类的属性完全没问题
    
    @Override
    public void test() {
        System.out.println(name + "我是匿名内部类的实现!");   //直接使用父类中的name变量
    }
};

同样的,接口也可以通过这种匿名内部类的形式,直接创建一个匿名的接口实现类:

java 复制代码
public static void main(String[] args) {
    Study study = new Study() {
        @Override
        public void study() {
            System.out.println("我是学习方法!");
        }
    };
    study.study();
}

当然,并不是说只有抽象类和接口才可以像这样创建匿名内部类,普通的类也可以,只不过意义不大,一般情况下只是为了进行一些额外的初始化工作而已。

Lambda表达式

前面我们介绍了匿名内部类,我们可以通过这种方式创建一个临时的实现子类。

特别的,如果一个接口中有且只有一个待实现的抽象方法,那么我们可以将匿名内部类简写为Lambda表达式:

java 复制代码
public static void main(String[] args) {
    Study study = () -> System.out.println("我是学习方法!");   //是不是感觉非常简洁!
  	study.study();
}

在初学阶段,为了简化学习,各位小伙伴就认为Lambda表达式就是匿名内部类的简写就行了(Lambda表达式的底层其实并不只是简简单单的语法糖替换,感兴趣的可以在新特性篇视频教程中了解)

那么它是一个怎么样的简写规则呢?我们来看一下Lambda表达式的具体规范:

  • 标准格式为:([参数类型 参数名称,]...) ‐> { 代码语句,包括返回值 }
  • 和匿名内部类不同,Lambda仅支持接口,不支持抽象类
  • 接口内部必须有且仅有一个抽象方法(可以有多个方法,但是必须保证其他方法有默认实现,必须留一个抽象方法出来)

比如我们之前写的Study接口,只要求实现一个无参无返回值的方法,所以说直接就是最简单的形式:

java 复制代码
() -> System.out.println("我是学习方法!");   //跟之前流程控制一样,如果只有一行代码花括号可省略

当然,如果有一个参数和返回值的话:

java 复制代码
public static void main(String[] args) {
    Study study = (a) -> {
        System.out.println("我是学习方法");
        return "今天学会了"+a;    //实际上这里面就是方法体,该咋写咋写
    };
    System.out.println(study.study(10));
}

注意,如果方法体中只有一个返回语句,可以直接省去花括号和return关键字:

java 复制代码
Study study = (a) -> {
    return "今天学会了"+a;   //这种情况是可以简化的
};
java 复制代码
Study study = (a) -> "今天学会了"+a;

如果参数只有一个,那么可以省去小括号:

java 复制代码
Study study = a -> "今天学会了"+a;

是不是感觉特别简洁,实际上我们程序员追求的就是写出简洁高效的代码,而Java也在朝这个方向一直努力,近年来从Java 9开始出现的一些新语法基本都是各种各样的简写版本。

如果一个方法的参数需要的是一个接口的实现:

java 复制代码
public static void main(String[] args) {
    test(a -> "今天学会了"+a);   //参数直接写成lambda表达式
}

private static void test(Study study){
    study.study(10);
}

当然,这还只是一部分,对于已经实现的方法,如果我们想直接作为接口抽象方法的实现,我们还可以使用方法引用。

方法引用

方法引用就是将一个已实现的方法,直接作为接口中抽象方法的实现(当然前提是方法定义得一样才行)

java 复制代码
public interface Study {
    int sum(int a, int b);   //待实现的求和方法
}

那么使用时候,可以直接使用Lambda表达式:

java 复制代码
public static void main(String[] args) {
    Study study = (a, b) -> a + b;
}

只不过还能更简单,因为Integer类中默认提供了求两个int值之和的方法:

java 复制代码
//Integer类中就已经有对应的实现了
public static int sum(int a, int b) {
    return a + b;
}

此时,我们可以直接将已有方法的实现作为接口的实现:

java 复制代码
public static void main(String[] args) {
    Study study = (a, b) -> Integer.sum(a, b);   //直接使用Integer为我们通过好的求和方法
    System.out.println(study.sum(10, 20));
}

我们发现,Integer.sum的参数和返回值,跟我们在Study中定义的完全一样,所以说我们可以直接使用方法引用:

java 复制代码
public static void main(String[] args) {
    Study study = Integer::sum;    //使用双冒号来进行方法引用,静态方法使用 类名::方法名 的形式
    System.out.println(study.sum(10, 20));
}

方法引用其实本质上就相当于将其他方法的实现,直接作为接口中抽象方法的实现。任何方法都可以通过方法引用作为实现:

java 复制代码
public interface Study {
    String study();
}

如果是普通从成员方法,我们同样需要使用对象来进行方法引用:

java 复制代码
public static void main(String[] args) {
    Main main = new Main();
    Study study = main::lbwnb;   //成员方法因为需要具体对象使用,所以说只能使用 对象::方法名 的形式
}

public String lbwnb(){
    return "卡布奇诺今犹在,不见当年倒茶人。";
}

因为现在只需要一个String类型的返回值,由于String的构造方法在创建对象时也会得到一个String类型的结果,所以说:

java 复制代码
public static void main(String[] args) {
    Study study = String::new;    //没错,构造方法也可以被引用,使用new表示
}

反正只要是符合接口中方法的定义的,都可以直接进行方法引用,对于Lambda表达式和方法引用,在Java新特性介绍篇视频教程中还有详细的讲解,这里就不多说了。


异常机制

在理想的情况下,我们的程序会按照我们的思路去运行,按理说是不会出现问题的,但是,代码实际编写后并不一定是完美的,可能会有我们没有考虑到的情况,如果这些情况能够正常得到一个错误的结果还好,但是如果直接导致程序运行出现问题了呢?

java 复制代码
public static void main(String[] args) {
    test(1, 0);   //当b为0的时候,还能正常运行吗?
}

private static int test(int a, int b){
    return a/b;   //没有任何的判断而是直接做计算
}

此时我们可以看到,出现了运算异常:

image-20220924164357033

那么这个异常到底是什么样的一种存在呢?当程序运行出现我们没有考虑到的情况时,就有可能出现异常或是错误!

异常的类型

我们在之前其实已经接触过一些异常了,比如数组越界异常,空指针异常,算术异常等,他们其实都是异常类型,我们的每一个异常也是一个类,他们都继承自Exception类!异常类型本质依然类的对象,但是异常类型支持在程序运行出现问题时抛出(也就是上面出现的红色报错)也可以提前声明,告知使用者需要处理可能会出现的异常!

异常的第一种类型是运行时异常,如上述的列子,在编译阶段无法感知代码是否会出现问题,只有在运行的时候才知道会不会出错(正常情况下是不会出错的),这样的异常称为运行时异常,异常也是由类定义的,所有的运行时异常都继承自RuntimeException

java 复制代码
public static void main(String[] args) {
    Object object = null;
    object.toString();   //这种情况就会出现运行时异常
}
image-20220924164637887

又比如下面的这种情况:

java 复制代码
public static void main(String[] args) {
    Object object = new Object();
    Main main = (Main) object;
}
image-20220924164844005

异常的另一种类型是编译时异常,编译时异常明确指出可能会出现的异常,在编译阶段就需要进行处理(捕获异常)必须要考虑到出现异常的情况,如果不进行处理,将无法通过编译!默认继承自Exception类的异常都是编译时异常。

java 复制代码
protected native Object clone() throws CloneNotSupportedException;

比如Object类中定义的clone方法,就明确指出了在运行的时候会出现的异常。

还有一种类型是错误,错误比异常更严重,异常就是不同寻常,但不一定会导致致命的问题,而错误是致命问题,一般出现错误可能JVM就无法继续正常运行了,比如OutOfMemoryError就是内存溢出错误(内存占用已经超出限制,无法继续申请内存了)

java 复制代码
public static void main(String[] args) {
    test();
}

private static void test(){
    test();
}

比如这样的一个无限递归的方法,会导致运行过程中无限制地向下调用方法,导致栈溢出:

image-20220924165500108

这种情况就是错误了,已经严重到整个程序都无法正常运行了。又比如:

java 复制代码
public static void main(String[] args) {
    Object[] objects = new Object[Integer.MAX_VALUE];   //这里申请一个超级大数组
}

实际上我们电脑的内存是有限的,不可能无限制地使用内存来存放变量,所以说如果内存不够用了,会直接:

image-20220924165657392

此时没有更多的可用内存供我们的程序使用,那么程序也就没办法继续运行下去了,这同样是一个很严重的错误。

当然,我们这一块主要讨论的目录依然是异常。

自定义异常

异常其实就两大类,一个是编译时异常,一个是运行时异常,我们先来看编译时异常。

java 复制代码
public class TestException extends Exception{
    public TestException(String message){
        super(message);   //这里我们选择使用父类的带参构造,这个参数就是异常的原因
    }
}

编译时异常只需要继承Exception就行了,编译时异常的子类有很多很多,仅仅是SE中就有700多个。

image-20220924202450589

异常多种多样,不同的异常对应着不同的情况,比如在类型转换时出错那么就是类型转换异常,如果是使用一个值为null的变量调用方法,那么就会出现空指针异常。

运行时异常只需要继承RuntimeException就行了:

java 复制代码
public class TestException extends RuntimeException{
    public TestException(String message){
        super(message);
    }
}

RuntimeException继承自Exception,Exception继承自Throwable:

image-20220924203130042

运行时异常同同样也有很多,只不过运行时异常和编译型异常在使用时有一些不同,我们会在后面的学习中慢慢认识。

当然还有一种类型是Error,它是所有错误的父类,同样是继承自Throwable的。

抛出异常

当别人调用我们的方法时,如果传入了错误的参数导致程序无法正常运行,这时我们就可以手动抛出一个异常来终止程序继续运行下去,同时告知上一级方法执行出现了问题:

java 复制代码
public static int test(int a, int b) {
    if(b == 0)
        throw new RuntimeException("被除数不能为0");  //使用throw关键字来抛出异常
    return a / b;
}

异常的抛出同样需要创建一个异常对象出来,我们抛出异常实际上就是将这个异常对象抛出,异常对象携带了我们抛出异常时的一些信息,比如是因为什么原因导致的异常,在RuntimeException的构造方法中我们可以写入原因。

当出现异常时:

image-20220924200817314

程序会终止,并且会打印栈追踪信息,因为各位小伙伴才初学,还不知道什么是栈,我们这里就简单介绍一下,实际上方法之间的调用是有层级关系的,而当异常发生时,方法调用的每一层都会在栈追踪信息中打印出来,比如这里有两个at,实际上就是在告诉我们程序运行到哪个位置时出现的异常,位于最上面的就是发生异常的最核心位置,我们代码的第15行。

并且这里会打印出当前抛出的异常类型和我们刚刚自定义异常信息。

注意,如果我们在方法中抛出了一个非运行时异常,那么必须告知函数的调用方我们会抛出某个异常,函数调用方必须要对抛出的这个异常进行对应的处理才可以:

java 复制代码
private static void test() throws Exception {    //使用throws关键字告知调用方此方法会抛出哪些异常,请调用方处理好
    throw new Exception("我是编译时异常!");
}

注意,如果不同的分支条件会出现不同的异常,那么所有在方法中可能会抛出的异常都需要注明:

java 复制代码
private static void test(int a) throws FileNotFoundException, ClassNotFoundException {  //多个异常使用逗号隔开
    if(a == 1)
        throw new FileNotFoundException();
    else 
        throw new ClassNotFoundException();
}

当然,并不是只有非运行时异常可以像这样明确指出,运行时异常也可以,只不过不强制要求:

java 复制代码
private static void test(int a) throws RuntimeException {
    throw new RuntimeException();
}

至于如何处理明确抛出的异常,我们会下一个部分中进行讲解。

最后再提一下,我们在重写方法时,如果父类中的方法表明了会抛出某个异常,只要重写的内容中不会抛出对应的异常我们可以直接省去:

java 复制代码
@Override
protected Object clone() {
    return new Object();
}

异常的处理

当程序没有按照我们理想的样子运行而出现异常时(默认会交给JVM来处理,JVM发现任何异常都会立即终止程序运行,并在控制台打印栈追踪信息)现在我们希望能够自己处理出现的问题,让程序继续运行下去,就需要对异常进行捕获,比如:

java 复制代码
public static void main(String[] args) {
    try {    //使用try-catch语句进行异常捕获
        Object object = null;
        object.toString();
    } catch (NullPointerException e){   //因为异常本身也是一个对象,catch中实际上就是用一个局部变量去接收异常

    }
    System.out.println("程序继续正常运行!");
}

我们可以将代码编写到try语句块中,只要是在这个范围内发生的异常,都可以被捕获,使用catch关键字对指定的异常进行捕获,这里我们捕获的是NullPointerException空指针异常:

image-20220924195434572

可以看到,当我们捕获异常之后,程序可以继续正常运行,并不会像之前一样直接结束掉。

注意,catch中捕获的类型只能是Throwable的子类,也就是说要么是抛出的异常,要么是错误,不能是其他的任何类型。

我们可以在catch语句块中对捕获到的异常进行处理:

java 复制代码
public static void main(String[] args) {
    try {
        Object object = null;
        object.toString();
    } catch (NullPointerException e){
        e.printStackTrace();   //打印栈追踪信息
        System.out.println("异常错误信息:"+e.getMessage());   //获取异常的错误信息
    }
    System.out.println("程序继续正常运行!");
}
image-20220924201405697

如果某个方法明确指出会抛出哪些异常,除非抛出的异常是一个运行时异常,否则我们必须要使用try-catch语句块进行异常的捕获,不然就无法通过编译:

java 复制代码
public static void main(String[] args) {
    test(10);    //必须要进行异常的捕获,否则报错
}

private static void test(int a) throws IOException {  //明确会抛出IOException
    throw new IOException();
}

当然,如果我们确实不想在当前这个方法中进行处理,那么我们可以继续踢皮球,抛给上一级:

java 复制代码
public static void main(String[] args) throws IOException {  //继续编写throws往上一级抛
    test(10);
}

private static void test(int a) throws IOException {
    throw new IOException();
}

注意,如果已经是主方法了,那么就相当于到顶层了,此时发生异常再往上抛出的话,就会直接交给JVM进行处理,默认会让整个程序终止并打印栈追踪信息。

注意,如果我们要捕获的异常,是某个异常的父类,那么当发生这个异常时,同样可以捕获到:

java 复制代码
public static void main(String[] args) throws IOException {
    try {
        int[] arr = new int[1];
        arr[1] = 100;    //这里发生的是数组越界异常,它是运行时异常的子类
    } catch (RuntimeException e){  //使用运行时异常同样可以捕获到
        System.out.println("捕获到异常");
    }
}

当代码可能出现多种类型的异常时,我们希望能够分不同情况处理不同类型的异常,就可以使用多重异常捕获:

java 复制代码
try {
  //....
} catch (NullPointerException e) {
            
} catch (IndexOutOfBoundsException e){

} catch (RuntimeException e){
            
}

但是要注意一下顺序:

java 复制代码
try {
  //....
} catch (RuntimeException e){  //父类型在前,会将子类的也捕获

} catch (NullPointerException e) {   //永远都不会被捕获

} catch (IndexOutOfBoundsException e){   //永远都不会被捕获

}

只不过这样写好像有点丑,我们也可以简写为:

java 复制代码
try {
     //....
} catch (NullPointerException | IndexOutOfBoundsException e) {  //用|隔开每种类型即可
		
}

如果简写的话,那么发生这些异常的时候,都会采用统一的方式进行处理了。

最后,当我们希望,程序运行时,无论是否出现异常,都会在最后执行任务,可以交给finally语句块来处理:

java 复制代码
try {
    //....
}catch (Exception e){
            
}finally {
  	System.out.println("lbwnb");   //无论是否出现异常,都会在最后执行
}

try语句块至少要配合catchfinally中的一个:

java 复制代码
try {
    int a = 10;
    a /= 0;
} finally {  //不捕获异常,程序会终止,但在最后依然会执行下面的内容
    System.out.println("lbwnb"); 
}

思考: trycatchfinally执行顺序?

断言表达式

我们可以使用断言表达式来对某些东西进行判断,如果判断失败会抛出错误,只不过默认情况下没有开启断言,我们需要在虚拟机参数中手动开启一下:

image-20220924220327591

开启断言之后,我们就可以开始使用了。

断言表达式需要使用到assert关键字,如果assert后面的表达式判断结果为false,将抛出AssertionError错误。

java 复制代码
public static void main(String[] args) {
    assert false;
}

比如我们可以判断变量的值,如果大于10就抛出错误:

java 复制代码
public static void main(String[] args) {
    int a = 10;
    assert a > 10;
}
image-20220924220704026

我们可以在表达式的后面添加错误信息:

java 复制代码
public static void main(String[] args) {
    int a = 10;
    assert a > 10 : "我是自定义的错误信息";
}

这样就会显示到错误后面了:

image-20220924220813609

断言表达式一般只用于测试,我们正常的程序中一般不会使用,这里只做了解就行了。


常用工具类介绍

前面我们学习了包装类、数组和字符串,我们接着来看看常用的一些工具类。工具类就是专门为一些特定场景编写的,便于我们去使用的类,工具类一般都会内置大量的静态方法,我们可以通过类名直接使用。

数学工具类

Java提供的运算符实际上只能进行一些在小学数学中出现的运算,但是如果我们想要进行乘方、三角函数之类的高级运算,就没有对应的运算符能够做到,而此时我们就可以使用数学工具类来完成。

java 复制代码
public static void main(String[] args) {
  	//Math也是java.lang包下的类,所以说默认就可以直接使用
    System.out.println(Math.pow(5, 3));   //我们可以使用pow方法直接计算a的b次方
  
  	Math.abs(-1);    //abs方法可以求绝对值
  	Math.max(19, 20);    //快速取最大值
  	Math.min(2, 4);   //快速取最小值
  	Math.sqrt(9);    //求一个数的算术平方根
}

当然,三角函数肯定也是安排上了的:

java 复制代码
Math.sin(Math.PI / 2);     //求π/2的正弦值,这里我们可以使用预置的PI进行计算
Math.cos(Math.PI);       //求π的余弦值
Math.tan(Math.PI / 4);    //求π/4的正切值

Math.asin(1);     //三角函数的反函数也是有的,这里是求arcsin1的值
Math.acos(1);
Math.atan(0);

可能在某些情况下,计算出来的浮点数会得到一个很奇怪的结果:

java 复制代码
public static void main(String[] args) {
    System.out.println(Math.sin(Math.PI));   //计算 sinπ 的结果
}
image-20220923231536032

正常来说,sinπ的结果应该是0才对,为什么这里得到的是一个很奇怪的数?这个E是干嘛的,这其实是科学计数法的10,后面的数就是指数,上面的结果其实就是:

  • 1.2246467991473532 \times 10^{-16}

其实这个数是非常接近于0,这是因为精度问题导致的,所以说实际上结果就是0。

我们也可以快速计算对数函数:

java 复制代码
public static void main(String[] args) {
    Math.log(Math.E);    //e为底的对数函数,其实就是ln,我们可以直接使用Math中定义好的e
    Math.log10(100);     //10为底的对数函数
    //利用换底公式,我们可以弄出来任何我们想求的对数函数
    double a = Math.log(4) / Math.log(2);   //这里是求以2为底4的对数,log(2)4 = ln4 / ln2
    System.out.println(a);
}

还有一些比较特殊的计算:

java 复制代码
public static void main(String[] args) {
    Math.ceil(4.5);    //通过使用ceil来向上取整
    Math.floor(5.6);   //通过使用floor来向下取整
}

向上取整就是找一个大于当前数字的最小整数,向下取整就是砍掉小数部分。注意,如果是负数的话,向上取整就是去掉小数部分,向下取整就是找一个小于当前数字的最大整数。

这里我们再介绍一下随机数的生成,Java中想要生成一个随机数其实也很简单,我们需要使用Random类来生成(这个类时java.util包下的,需要手动导入才可以)

java 复制代码
public static void main(String[] args) {
    Random random = new Random();   //创建Random对象
    for (int i = 0; i < 30; i++) {
        System.out.print(random.nextInt(100)+" ");  //nextInt方法可以指定创建0 - x之内的随机数
    }
}

结果为,可以看到确实是一堆随机数:

image-20220923234642670

只不过,程序中的随机并不是真随机,而是根据某些东西计算出来的,只不过计算过程非常复杂,能够在一定程度上保证随机性(根据爱因斯坦理论,宏观物质世界不存在真随机,看似随机的事物只是现目前无法计算而已,唯物主义的公理之一就是任何事物都有因果关系)

数组工具类

前面我们介绍了数组,但是我们发现,想要操作数组实在是有点麻烦,比如我们要打印一个数组,还得一个一个元素遍历才可以,那么有没有一个比较方便的方式去使用数组呢?我们可以使用数组工具类Arrays。

这个类也是java.util包下类,它用于便捷操作数组,比如我们想要打印数组,可以直接通过toString方法转换字符串:

java 复制代码
public static void main(String[] args) {
    int[] arr = new int[]{1, 4, 5, 8, 2, 0, 9, 7, 3, 6};
    System.out.println(Arrays.toString(arr));
}
image-20220923235747731

是不是感觉非常方便?这样我们直接就可以打印数组了!

除了这个方法,它还支持将数组进行排序:

java 复制代码
public static void main(String[] args) {
    int[] arr = new int[]{1, 4, 5, 8, 2, 0, 9, 7, 3, 6};
    Arrays.sort(arr);    //可以对数组进行排序,将所有的元素按照从小到大的顺序排放
    System.out.println(Arrays.toString(arr));
}

感兴趣的小伙伴可以在数据结构与算法篇视频教程中了解多种多样的排序算法,这里的排序底层实现实际上用到了多种排序算法。

数组中的内容也可以快速进行填充:

java 复制代码
public static void main(String[] args) {
    int[] arr = new int[10];
    Arrays.fill(arr, 66);
    System.out.println(Arrays.toString(arr));
}

我们可以快速地对一个数组进行拷贝:

java 复制代码
public static void main(String[] args) {
    int[] arr = new int[]{1, 2, 3, 4, 5};
    int[] target = Arrays.copyOf(arr, 5);
    System.out.println(Arrays.toString(target));   //拷贝数组的全部内容,并生成一个新的数组对象
    System.out.println(arr == target);
}
java 复制代码
public static void main(String[] args) {
    int[] arr = new int[]{1, 2, 3, 4, 5};
    int[] target = Arrays.copyOfRange(arr, 3, 5);   //也可以只拷贝某个范围内的内容
    System.out.println(Arrays.toString(target));
    System.out.println(arr == target);
}

我们也可以将一个数组中的内容拷贝到其他数组中:

java 复制代码
public static void main(String[] args) {
    int[] arr = new int[]{1, 2, 3, 4, 5};
    int[] target = new int[10];
    System.arraycopy(arr, 0, target, 0, 5);   //使用System.arraycopy进行搬运
    System.out.println(Arrays.toString(target));
}

对于一个有序的数组(从小到大排列)我们可以使用二分搜索快速找到对应的元素在哪个位置:

java 复制代码
public static void main(String[] args) {
    int[] arr = new int[]{1, 2, 3, 4, 5};
    System.out.println(Arrays.binarySearch(arr, 5));   //二分搜索仅适用于有序数组
}

这里提到了二分搜索算法,我们会在后面的实战练习中进行讲解。

那要是现在我们使用的是多维数组呢?因为现在数组里面的每个元素就是一个数组,所以说toString会出现些问题:

java 复制代码
public static void main(String[] args) {
    int[][] array = new int[][]{{2, 8, 4, 1}, {9, 2, 0, 3}};
    System.out.println(Arrays.toString(array));
}
image-20220924114142785

只不过别担心,Arrays也支持对多维数组进行处理:

java 复制代码
public static void main(String[] args) {
    int[][] array = new int[][]{{2, 8, 4, 1}, {9, 2, 0, 3}};
    System.out.println(Arrays.deepToString(array));    //deepToString方法可以对多维数组进行打印
}

同样的,因为数组本身没有重写equals方法,所以说无法判断两个不同的数组对象中的每一个元素是否相同,Arrays也为一维数组和多维数组提供了相等判断的方法:

java 复制代码
public static void main(String[] args) {
    int[][] a = new int[][]{{2, 8, 4, 1}, {9, 2, 0, 3}};
    int[][] b = new int[][]{{2, 8, 4, 1}, {9, 2, 0, 3}};
    System.out.println(Arrays.equals(a, b));   //equals仅适用于一维数组
    System.out.println(Arrays.deepEquals(a, b));   //对于多维数组,需要使用deepEquals来进行深层次判断
}

这里肯定有小伙伴疑问了,不是说基本类型的数组不能转换为引用类型的数组吗?为什么这里的deepEquals接受的是Object[]也可以传入参数呢?这是因为现在是二维数组,二维数组每个元素都是一个数组,而数组本身的话就是一个引用类型了,所以说可以转换为Object类型,但是如果是一维数组的话,就报错:

image-20220924115440998

总体来说,这个工具类对于我们数组的使用还是很方便的。


实战练习

到目前为止,关于面向对象相关的内容我们已经学习了非常多了,接着依然是练习题。

冒泡排序算法

有一个int数组,但是数组内的数据是打乱的,现在我们需要将数组中的数据按从小到大的顺序进行排列:

java 复制代码
public static void main(String[] args) {
    int[] arr = new int[]{3, 5, 7, 2, 9, 0, 6, 1, 8, 4};
}

请你设计一个Java程序将这个数组中的元素按照顺序排列。

二分搜索算法

现在有一个从小到大排序的数组,给你一个目标值target,现在我们想要找到这个值在数组中的对应下标,如果数组中没有这个数,请返回-1

java 复制代码
public static void main(String[] args) {
    int[] arr = {1, 3, 4, 6, 7, 8, 10, 11, 13, 15};
    int target = 3;
}

请你设计一个Java程序实现这个功能。

青蛙跳台阶问题

现在一共有n个台阶,一只青蛙每次只能跳一阶或是两阶,那么一共有多少种跳到顶端的方案?

例如n=2,那么一共有两种方案,一次性跳两阶或是每次跳一阶。

现在请你设计一个Java程序,计算当台阶数为n的情况下,能够有多少种方案到达顶端。

回文串判断

“回文串”是一个正读和反读都一样的字符串,请你实现一个Java程序,判断用户输入的字符串(仅出现英文字符)是否为“回文”串。

ABCBA 就是一个回文串,因为正读反读都是一样的

ABCA 就不是一个回文串,因为反着读不一样

汉诺塔求解

什么是汉诺塔?

汉诺塔(Tower of Hanoi),又称河内塔,是一个源于印度古老传说的益智玩具大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始

按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。

img

这三根柱子我们就依次命名为A、B、C,现在请你设计一个Java程序,计算N阶(n片圆盘)汉诺塔移动操作的每一步。

正在加载页面,请稍后...