跨越编程界限:C++到JavaSE的平滑过渡

JDK安装

  1. 安装JDK

  2. 配置环境变量:

    Path 内添加 C:\Program Files\Java\jdk1.8.0_201\bin

    添加 JAVA_HOME C:\Program Files\Java\jdk1.8.0_201

    添加 CLASSPATH .;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar

第一个Java程序

HelloWorld.java

public class HelloWorld {
	public static void main(String[] args) {
		System.out.println("Hello world");
	}
}

保存退出,打开终端

输入:

javac HelloWorld.java

此时多了一个 HelloWorld.class 文件,.class 叫做字节码文件

输入:

java HelloWorld

输出Hello world

解释

  1. public 访问修饰限定符
  2. class 定义类的关键字
  3. HelloWorld 类名,建议使用大驼峰

如果一个类是 public 修饰的,那么这个类的类名要和文件名一样。

  1. main 函数固定写法 public static void main(String[] args)

  2. System.out.println("Hello world");

    另外两个打印函数:

    System.out.print("Hello world");

    不带ln,区别在于不换行

    System.out.printf("%s", "Hello world");

    格式化输出,同C语言

  3. String[] args

    Java 中的数组使用类型加方括号的形式,方括号中不写数字。

    该参数为命令行参数

Java 程序运行在哪?

Java 程序运行在 Java虚拟机——JVM 之上,同样一个字节码文件可以在任意一台安装了jdk的机器上运行。

注释

单行和多行注释和C语言相同。

文档注释:/** 文档注释 */ 常见于方法和类之上描述方法和类的作用)可以被javadoc工具解析,生成一套以网页文件形式体现的程序说明文档

/**
 * @author cero
 * @version 1.1
 */

注意当在文件中写了中文,然后使用 javac 编译的时候报错,这往往是编码问题

现在我们写代码的环境一般都是utf-8,但是 javac 是 GBK 编码。

在编译时指定编码:-encoding utf-8 即可解决。


使用javadoc工具从Java源码中抽离出注释:

javadoc -d myHello -author -version -encoding UTF-8 -charset UTF-8 HelloWorld.java

-d 创建目录 myHello 为目录名

-author 显示作者

-version 显示版本号

-encoding UTF-8 -charset UTF-8 字符集修改为 UTF-8


IDEA 快捷键:

ctrl + / 行注释

ctrl + shift + / 块注释


标识符

硬性规则

标识符可以包含数字、字母、下划线、美元符号$;不能以数字开头,也不能是关键字;且严格区分大小写。

软性规则

类名:采用大驼峰

变量和方法的命名:采用小驼峰


IDEA快捷键:

  • psvmmain:生成 main 函数。
  • sout:生成 println 函数,也可以写一个东西然后加个.sout

数据类型

Java数据类型

基本数据类型:

  • 整数:byte short int long
  • 小数:float double
  • 布尔:boolean
  • 字符:char

引用数据类型:

  • 数组,String,类,接口,枚举

注意

  • 局部变量在使用前一定要初始化,否则会直接报错,这一点不同于 C 语言,体现了 Java 的安全性。
  • 赋值时小类型可以赋值给大类型,反之不行
  • 赋值时,字面值常量的大小不能超过类型的范围,否则会报错
  • 在 Java 中,没有无符号和有符号的说法
  • int 占 4 字节,long 占 8 字节,short 占 2 字节,byte 是区别于 C 语言的一个新的类型,只占 1 个字节
  • Java 中的 char2 字节,所以 Java 中的 char 可以存中文字符
  • Java 中不存在 0 为假,非 0 为真
  • Java 中存在类型转换(包括显式和隐式)和类型提升
  • 所有小于 4 字节的元素在运算时都会提升为 4 字节

打印int的最大值和最小值:

System.out.println(Integer.MAX_VALUE);
System.out.println(Integer.MIN_VALUE);

其中的 Integer 是包装类——基本数据类型所对应的类类型

int 的类类型是 Integerchar 的类类型是 Character,其他的基本数据类型对应的类类型都是首字母大写


字符串

字符串拼接

字符串String之间使用 + 号进行拼接,其他类型,如 intString 进行 + 运算也是拼接,而不会进行数字计算,例:

public class TestDemo {
    public static void main(String[] args) {
        System.out.println("10 + 20 = " + 10 + 20);
        System.out.println("10 + 20 = " + (10 + 20));
        System.out.println(10 + 20 + " = 10 + 20");
    }
}

结果:

10 + 20 = 1020
10 + 20 = 30
30 = 10 + 20

数字,字符串互转

public class TestDemo {
    public static void main(String[] args) {
        // 数字转字符串
        int num = 10;
        // 1. 使用 + 号拼接
        String str1 = num + "";
        System.out.println(str1);
        // 2. 使用 valueOf
        String str2 = String.valueOf(num);
        System.out.println(str2);

        // 字符串转数字
        String str = "123";
        int a = Integer.parseInt(str);
        System.out.println(a + 1);
    }
}

字符串比较

Java 的字符串比较要用 equals() 方法

import java.util.Scanner;

public class TestDemo {
    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        String password = scan.next();
        if (password.equals("123")) {
            System.out.println("输入正确");
        } else {
            System.out.println("输入错误");
        }
    }
}

如果你用 == ,那么实际上比较的是两个 String 的地址

数组

定义方式

int[] array1 = {1, 2, 3, 4, 5};
int[] array2 = new int[]{1, 2, 3, 4, 5};
int[] array3 = new int[10]; // 大小为10,以默认值初始化
System.out.println(array1.length); // 通过.length获取数组长度

前两种写法本质上没有区别,都是手动输入初始化数据,数组长度编译器自动推导,[]内不能写数字

第三种方式使用 new 开辟一个指定大小的数组,以默认值初始化(默认值也就是对应的 0-值,如果是引用类型,那么默认值就是null)

  • 因为 Java 的数组是引用类型,所以数组可以理解为是动态分配的,里面的数据存储在内存中的堆区。
  • Java 会严格检测数组访问越界,一旦越界,就会抛异常。

数组遍历

  1. 使用普通的 for/while 循环

  2. Java 支持 for-each,和 C++11 的语法一样。

  3. 借助 Java 提供的 Arrays 工具类

    import java.lang.reflect.Array;
    import java.util.Arrays;
    
    public class TestDemo {
        public static void main(String[] args) {
            int[] array = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
            System.out.println(Arrays.toString(array));
        }
    }
    

    输出:

    [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    

    Arrays 中的 toString() 方法可以将数组转为字符串,而且还用 ,[] 加了格式。

数组拷贝

  1. for 循环

  2. Arrays.copyOf() 方法:

    public static int[] copyOf(int[] original,
                               int newLength)
        // original - 要复制的数组
        // newLength - 要返回的副本的长度
        // newLength 大于original数组长度,则返回的数组会在末尾使用0填充,如果小于,则截断
        // 返回:原始数组的副本
    
    import java.util.Arrays;
    
    public class TestDemo {
        public static void main(String[] args) {
            int[] array = {1, 2, 3, 4};
            int[] copy = Arrays.copyOf(array, array.length);
            System.out.println(Arrays.toString(copy));
        }
    }
    
  3. System.arraycopy() 方法:

    copyOf 的实现其实用的就是这个方法

    public static native void arraycopy(Object src,  int  srcPos,
                                            Object dest, int destPos,
                                            int length);
    // src - 源数组.
    // srcPos - 源数组中的起始位置.
    // dest - 目标数组.
    // destPos - 目标数组中的起始位置.
    // length - 要复制的数组元素数.
    

    我们看不到这个函数的具体实现,因为它由 native 修饰,是一个本地方法,也就是说,它其实是由 C/C++ 实现的。

    import java.util.Arrays;
    
    public class TestDemo {
        public static void main(String[] args) {
            int[] array = {1, 2, 3, 4};
            int[] copy = new int[array.length];
            System.arraycopy(array, 0, copy, 0, array.length);
            System.out.println(Arrays.toString(copy));
        }
    }
    
  4. clone() 方法

    这是从 Object 继承的方法,能直接对自己拷贝出一个对象并返回

    import java.util.Arrays;
    
    public class TestDemo {
        public static void main(String[] args) {
            int[] array = {1, 2, 3, 4};
            int[] copy = array.clone();
            System.out.println(Arrays.toString(copy));
        }
    }
    
  5. Arrays.copyOfRange() 方法

    public static int[] copyOfRange(int[] original,
                                    int from,
                                    int to)
    // 从 original 数组的 [from, to) 范围的数据,返回副本数组
    

数组排序

使用 Arrays.sort()

该方法默认排升序,使用接口可以指定其他排序方式

二分查找

Arrays.binarySearch()

public static int binarySearch(int[] a,
                               int key)
    
public static int binarySearch(int[] a,
                               int fromIndex,
                               int toIndex,
                               int key)

一个是对整个数组的二分查找,一个是指定范围的二分查找。

返回找到的key的下标,如果没找到,则返回 (-(插入点) - 1) 的值。也就是说,找到则返回的是正数,没找到返回负数。

比较数组

public static boolean equals(int[] a,
                             int[] a2)

两个数组相等返回 true ,否则返回 false

数组填充

给数组填充指定的值

public static void fill(int[] a,
                        int val)
    
public static void fill(int[] a,
                    int fromIndex,
                    int toIndex,
                    int val)

二维数组

// 手动给值初始化
int[][] array1 = {{1, 2, 3}, {4, 5, 6}};
int[][] array2 = new int[][]{{1, 2, 3}, {4, 5, 6}};

int[][] array3 = new int[2][3]; // 一个2行3列的数组,默认值初始化

int[][] array4 = new int[3][]; // 可以不给列大小。比如这个3行的数组,每一行都是一个int[]类型的null引用

最后一种方式的定义的数组,可以在稍后给每一行分配不同大小的数组

array4[0] = new int[3];
array4[1] = new int[4];

运算符

  • &| 的左右表达式为 boolean 时,也表示逻辑运算,但与 && || 相比,它们不支持短路求值

  • Java 中的 >>> 是无符号右移,无论正数还是负数,高位都是补0。与之相对的 >> 是有符号右移,高位补的是符号位

标准输入

Java 标准输入首先需要 new 一个 Scanner 对象,IDEA 会帮我们自动 import Scanner

使用 System.in 来初始化,表示标准输入。

调用 nextInt() 方法读取一个 int

import java.util.Scanner;

public class TestDemo {
   public static void main(String[] args) {
       Scanner scanner = new Scanner(System.in);
       int n = scanner.nextInt();
   }
}

使用 next() 方法读取一个 String(以空格和换行作为分隔符)

nextLine() 方法是只以换行作为分隔符

import java.util.Scanner;

public class TestDemo {
    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        String name = scan.next();
        System.out.println("姓名:" + name);
    }
}

判断闰年:

import java.util.Scanner;

public class TestDemo {
    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        while (scan.hasNext()) {
            int year = scan.nextInt();
            if (year % 4 == 0 && year % 100 != 0 || year % 400 == 0) {
                System.out.println(year + "年是闰年");
            } else {
                System.out.println(year + "年是平年");
            }
        }
    }
}

注:这里使用 hasNext() 来循环输入,在 IDEA 下,使用 ctrl+d 终止。

猜数字:

import java.util.Random;
import java.util.Scanner;

public class TestDemo {
    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        Random random = new Random();
        int randomNum = random.nextInt(100) + 1; // 生成 [1, 100] 的一个随机int
        while (true) {
            System.out.println("请输入一个数字");
            int num = scan.nextInt();
            if (num > randomNum) {
                System.out.println("猜大了");
            } else if (num < randomNum) {
                System.out.println("猜小了");
            } else {
                System.out.println("猜中了");
                break;
            }
        }
    }
}

生成随机数需要先 new 一个 Random 对象,可以用固定的种子去初始化,如果不填就是随机种子。使用 nextInt(100) 可以生成 [0, 100) 的数。

方法

方法就是函数

  • 在 Java 中,方法必须写在类当中
  • 在 Java 中, 没有方法声明一说

Java 中的方法支持重载,Java 通过方法签名来标识一个方法。

方法签名:经过编译器编译修改过的方法的最终名字,具体为:方法全路径名+参数列表+返回值类型

public class TestDemo {
    public static int add(int a, int b) {
        return a + b;
    }

    public static double add(double a, double b) {
        return a + b;
    }

    public static void main(String[] args) {
        System.out.println(add(1, 2));
        System.out.println(add(1.1, 2.2));
    }
}

编译之后,使用 javap -v TestDemo 指令反汇编:

跨越编程界限:C++到JavaSE的平滑过渡_第1张图片

JVM的内存分布

  • 程序计数器(PC Register):只是一个很小的空间,保存下一条执行的指令的地址。
  • 虚拟机栈(JVM Stack):与方法调用相关的一些信息,每个方法在执行时都会先创建一个栈帧,栈帧中包含有:局部变量表、操作数栈、动态链接、返回地址以及其他的一些信息,保存的都是与方法执行时相关的一些信息。比如:局部变量。当方法运行结束后,栈帧就被销毁了,即栈帧中保存的数据也被销毁了。
  • 本地方法栈(Native Method Stack):本地方法栈与虚拟机栈的作用类似。只不过保存的内容是 Native 方法的局部变量。在有些版本的JVM 实现中(例如HotSpot),本地方法栈和虚拟机栈是一起的。
  • 堆(Heap):JVM所管理的最大内存区域.使用 new 创建的对象都是在堆上保存(例如前面的new int[]{1, 2, 3}),堆是随着程序开始运行时而创建,随着程序的退出而销毁,堆中的数据只要还有在使用,就不会被销毁。
  • 方法区(Method Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法编译出的字节码就是保存在这个区域。

其中,方法区和堆是由所有线程共享的,其他数据区是线程隔离的。

Java 堆上开辟的内存,不需要我们手动释放,JVM 本身有垃圾回收器,帮我们释放内存。

引用

如果我们直接打印一个对象:

int[] array = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
System.out.println(array);

输出:

[I@1b6d3586

这个结果就是这个数组对象的地址的哈希值。@ 前的 [I 表示这是一个 int 数组,[ 就是表示数组。

其中的 array 就是引用变量(简称引用),这里的引用是存在虚拟机栈上的,存的内容是数组对象的地址。数组的内容,即数组对象,存在堆上。

引用变量使用前也必须初始化,可以 new 一个对象来初始化,也可以是 null 初始化:

int[] array = null; // 一个不指向任何对象的引用
int[] array1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] array2 = array1;
// 指向同一个对象的两个引用
int[] array1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] array2 = {2, 3, 4, 5};
array1 = array2;
// array1和array2都指向都指向了第二个对象,此时第一个对象没有被引用,会被垃圾回收器自动回收

小练习

  1. 下面的程序输出什么?

    public static void func1(int[] arr) {
        arr = new int[]{11, 12, 13, 14};
    }
    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4};
        func1(arr);
        System.out.println(Arrays.toString(arr));
    }
    

    答案:[1, 2, 3, 4]

    解析:形参的改变不会影响实参,虽然形参指向了另一个对象,但是实参指向的还是原来的对象,所以不变。

  2. 下面的程序输出什么?

    public static void func2(int[] arr) {
        arr[0] = 99;
    }
    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4};
        func2(arr);
        System.out.println(Arrays.toString(arr));
    }
    

    答案:[99, 2, 3, 4]

    解析:形参的引用和实参的引用指向了同一个对象,形参使用[]+下标的方式修改了这个对象的一个值,最后打印的就是修改后的结果。

重写toString

我们知道打印一个引用变量会输出它的地址,我们也可以让它输出别的

查看 println 的源码:

public void println(Object x) {
    String s = String.valueOf(x);
    synchronized (this) {
        print(s);
        newLine();
    }
}

看到它调用了 valueOf(),那么我们再去查看 valueOf() 方法

public static String valueOf(Object obj) {
    return (obj == null) ? "null" : obj.toString();
}

可以看到,println 实际上是调用了转入参数的 toString,这个方法是从 Object 类继承的,我们可以重写。

例:

class Test {
    @Override
    public String toString() {
        System.out.println("一个重写的toString方法");
        return "haha";
    }
}

public class TestDemo {
    public static void main(String[] args) {
        System.out.println(new Test());
    }
}

输出

一个重写的toString方法
haha

类和对象

Java 的类和对象

软性要求

  • 一般一个文件当中只定义一个类
  • main 方法所在的类一般要使用 public 修饰
  • 不要轻易修改 public 修饰的类的名称,如要修改,要通过开发工具修改(IDEA 中右键文件名->Refactor->Rename File…)

硬性要求

  • public 修饰的类必须要和文件名相同
  • 访问修饰限定符要写在各个属性和方法的前面,这一风格和 C++ 不同

特性

  • Java 同样可以在属性后面用 = 来指定初始值,这称作就地初始化

  • 编译器会给每个类生成不带参的默认构造方法,如果你为一个类提供了构造方法,编译器便不再为这个类生成默认构造方法了。

  • IDEA 右键可以生成你想要的构造方法,get set方法等

  • this 还可以用来调用本类中的其他的构造方法,以 this() 的形式,必须写在一个构造方法的第一行

    • this 不能形成环,例:

      public Date() {
          this(2023, 11, 11);
      }
      
      public Date(int year, int month, int day) {
          this();
      }
      

      当对象创建的时候,要调用一个构造方法A,构造方法A要调用构造方法B,而B又要调用A,这就死循环了,所以不行。

  • 虽然可以在构造方法 new 自己类的对象,但在运行时会栈溢出

Java 构造一个对象的过程

  1. 检测对象对应的类是否加载了,如果没有加载则加载

  2. 为对象分配内存空间

  3. 处理并发安全问题

    比如:多个线程同时申请对象,JVM 要保证给对象分配的空间不冲突

  4. 初始化所分配的空间

    即:对象空间被申请好之后,对象中包含的成员已经设置好了初始值(零值)

  5. 设置对象头信息

  6. 调用合适的构造方法

封装

访问限定符

Java 提供了 4 种访问限定符

范围 private default protected public
同一包中的同一类
同一包中的不同类
不同包中的子类
不同包中的非子类
  • 一个方法或属性的前面没有指定访问限定符,那么它相当于被 default 修饰
  • default 权限叫做默认权限,又叫包访问权限,可以正好将可访问范围限定为当前这个包
  • protected 主要用在继承中,只要在同一个包或者是这个类的子类就可以访问
  • 访问权限除了可以限定类中成员的可见性,也可以控制类的可见性
  • 类只能用 public 修饰或默认修饰,被 public 修饰的类可以被任意 java 文件导入,默认修饰是类只能被同一包中的 java 文件导入

在面向对象体系中,提出了一个软件包的概念,即:为了更好的管理类,把多个类收集在一起成为一组,称为软件包。类似于文件夹。

Java 中也引入了包,包是对类、接口等的封装机制的体现,是一种对类或者接口等的很好的组织方式,比如:一个包中的类不想被其他包中的类使用。包还有一个重要的作用:在同一个工程中允许存在相同名称的类,只要处在不同的包中即可

import

import 就是用来导入某个包中的,比如 import java.util.Arrays 导入 java 包中的 util 包中的 Arrays 类。

也可以写 import java.util.* 表示导入 java.util 包下的所有类。

不写 import 也可以用其他包下的类,只是每次使用都要指定所在的包:

java.util.Date date = new java.util.Date();

注:如果导入了两或多个名称相同的类,则必须要在每次使用时都指定所在的包。

静态导入(不建议使用)

使用 import static 可以导入包中的静态方法和字段

比如:导入 Math 类下的静态方法

import static java.lang.Math.*;

public class TestDemo {
    public static void main(String[] args) {
        double result = sqrt(pow(3, 2) + pow(4, 2));
        System.out.println(result);
    }
}

本来我们调用 sqrt()pow() 方法是要在前面加 Math. 的,静态导入后就不需要了。

自己创建包

  • IDEA 右键一个文件名->New->Package

跨越编程界限:C++到JavaSE的平滑过渡_第2张图片

  • 包名一般以域名倒过来写,如 com.cero.demo1
  • 包名要和代码路径相匹配,例如创建 com.cero.demo1 的包,那么会存在一个对应的路径 com/cero/demo1 来存储代码

我们可以创建出这样一个结构:

跨越编程界限:C++到JavaSE的平滑过渡_第3张图片

  • 包中的java文件开头都会用 package 声明自己所在的包
  • 如果没有 package 语句,则该类被放到一个默认包中(src 之下)

TestDemo1.java

package com.cero.demo1;

public class TestDemo1 {
}

不同的包中的类也可以互相导入并使用:

TestDemo2.java

package com.cero.demo2;

import com.cero.demo1.TestDemo1;

public class TestDemo2 {
    TestDemo1 testDemo = new TestDemo1();
}

常见的包

  1. java.lang 系统常用基础类(String、Object),此包自 JDK1.1 后自动导入
  2. java.lang.reflect java 反射编程包
  3. java.net 网络编程开发包
  4. java.sql 数据库开发包
  5. java.util java 工具程序包 。(重要
  6. java.io I/O 编程开发包

static

静态成员变量

  • static 修饰的成员变量叫做静态成员变量,也叫类变量。存储在方法区。和 C++ 一样,是所有对象共享的。
    • 静态成员变量可以通过类名访问,也可以通过对象访问,更推荐通过类名访问
  • 静态成员变量的生命周期伴随整个类,也就是程序的声明周期,在进程启动时初始化,进程结束时销毁

特殊案例:

public class Student {
    String name;
    String sex;
    String no;
    public static String classes;
}
public class TestDemo {
    public static void main(String[] args) {
        Student student1 = null;
        student1.classes = "计算机231"; // 虽然 student1 引用 null,但是并不会抛出空指针异常
        System.out.println(student1.classes);
    }
}

静态方法

  • static 修饰的方法叫做静态方法
  • 静态方法没有 this 引用,不能访问非静态成员

静态成员变量初始化

静态成员变量可以就地初始化,也可以通过代码块初始化。

代码块

使用 {} 定义的一段代码称为代码块,根据代码块定义的位置以及关键字,又可分为以下四种

  1. 普通代码块(本地代码块)

    定义在方法中的代码块

    public class TestDemo {
        public static void main(String[] args) {
            {
                System.out.println("haha");
            }
        }
    }
    

    这种用法比较少。

  2. 实例代码块(构造代码块)

    写在类中,优先于构造方法执行,类似于 C++ 的初始化列表

    例:

    public class Student {
        private String name;
        private int age;
        private double score;
        public static String classes;
    
        {
            name = "haha";
            age = 20;
            score = 100;
            System.out.println("实例代码块,一般用来初始化实例(普通)成员变量");
        }
    
        public Student(String name, int age, double score) {
            this.name = name;
            this.age = age;
            this.score = score;
            System.out.println("调用带有3个参数的构造方法");
        }
    }
    
    public class TestDemo {
        public static void main(String[] args) {
            Student student1 = new Student("张三", 20, 88.5);
        }
    }
    

    输出:

    实例代码块,一般用来初始化实例(普通)成员变量
    调用带有3个参数的构造方法
    

    如果一个类有多个实例代码块,则按照定义的顺序执行

  3. 静态代码块

    一般用于初始化静态数据成员,比实例代码块更早执行

    static {
        classes = "计算机231";
        System.out.println("静态代码块,一般用于初始化静态数据成员");
    }
    

    输出

    静态代码块,一般用于初始化静态数据成员
    实例代码块,一般用来初始化实例(普通)成员变量
    调用带有3个参数的构造方法
    

    注意

    • 一个类的静态代码块最多只会执行一次,无论你 new 多少个这个类的对象
    • 如果一个类有多个静态代码块,则根据定义的顺序来执行
    • 实例代码块只在创建对象时执行,而静态代码块在第一次使用类的时候执行(可以不创建对象)
  4. 同步代码块(用不到)

内部类

定义在一个类的内部的类叫做内部类。当一个事物的内部有一部分需要一个完整的结构来描述,那么这个完整结构可以使用内部类。

内部类经过编译会生成独立的字节码文件 外部类名$内部类名.class

内部类分为四种

  1. 实例内部类(成员内部类)

    在外部需要使用 外部类名.内部类名 变量名 = 外部类对象的引用.new InnerClass() 的形式来实例化内部类对象

    class OuterClass {
        public int data1 = 10;
        private int data2 = 20;
        public static int data3 = 30;
        // 实例内部类
        class InnerClass {
            public int data4 = 40;
            private int data5 = 50;
            public  InnerClass() {
                System.out.println("InnerClass的构造方法");
            }
        }
    }
    
    public class TestDemo {
        public static void main(String[] args) {
            OuterClass outerClass = new OuterClass();
            OuterClass.InnerClass innerClass = outerClass.new InnerClass();
        }
    }
    

    输出:

    InnerClass的构造方法
    

    注意

    • 实例内部类中不能定义静态成员变量,但是可以定义常量(static final 修饰)

    • 实例内部类中不能定义静态方法

    • 实例内部类可以直接访问外部类的任何成员,如果实例内部类的成员变量和外部类的成员变量重名,优先使用内部类自己的。如果非要使用外部类的,语法为 外部类类名.this.变量名。这说明:实例内部类中不仅包含自己的this,也包含了外部类的this

      class OuterClass {
          public int data1 = 10;
          private int data2 = 20;
          public static int data3 = 30;
          
          class InnerClass {
              public int data4 = 40;
              private int data5 = 50;
              private int data3 = 60; // data3重名
              
              public  InnerClass() {
                  System.out.println("InnerClass的构造方法");
              }
      
              public void method() {
                  System.out.println(data3); // 打印自己的data3
                  System.out.println(OuterClass.this.data3); // 打印外部类的data3
              }
          }
      }
      
      public class TestDemo {
          public static void main(String[] args) {
              OuterClass.InnerClass innerClass = new OuterClass().new InnerClass();
              innerClass.method();
          }
      }
      

      输出

      InnerClass的构造方法
      60
      30
      
    • 外部类不能直接访问内部类的成员,但是可以通过实例化内部类对象来访问

      // 一个外部类的方法
      public void method() {
          InnerClass innerClass = new InnerClass();
          System.out.println(innerClass.data4);
      }
      
    • 实例内部类所处的位置与外部类的成员是相同的,所以也可以用访问限定符修饰

  2. 静态内部类:被 static 修饰

    你可以将静态内部类当成外部类的一个静态成员,所以静态内部类的实例化不依赖于对象

    class OuterClass {
        public int data1 = 10;
        private int data2 = 20;
        public static int data3 = 30;
        
        static class InnerClass {
            public int data4 = 40;
            private int data5 = 50;
            public static int data6 = 60;
    
            InnerClass() {
                System.out.println("InnerClass()");
            }
    
            public void method() {
                System.out.println("innerclass的method方法");
            }
        }
    }
    
    public class TestDemo {
        public static void main(String[] args) {
            // 静态内部类实例化
            OuterClass.InnerClass innerClass = new OuterClass.InnerClass();
        }
    }
    

    注意

    • 静态内部类没有外部类的this引用,所以不能直接访问外部类的非静态成员,如果要访问,可以先 new 一个外部类的对象出来
  3. 局部内部类:

    定义在方法中的内部类,几乎不用。

    只能在所在方法内部使用;不能被访问限定符修饰,static 修饰

  4. 匿名内部类

继承

Java 的父类与子类,构造

Java 中继承的关键字是 extends,Java 不支持多继承,即一个子类不可以有多个父类

class Animal {
    public String name;
    public int age;
    public String sex;

    public void eat() {
        System.out.println(this.name + "eat()");
    }
}
// 继承自Animal类
class Dog extends Animal {
    public void mew() {
        System.out.println(this.name + "cat::mew()");
    }
}
// 继承自Animal类
class Cat extends Animal {
    public void bark() {
        System.out.println("dog::bark()");
    }
}
  • 如果子类和父类中的成员重名,则优先访问自己的,如果要访问父类的,使用 super 关键字,super 可以看做是对子类对象的父类部分的引用

    class Base {
        public int a = 1;
        public int b = 2;
    }
    
    class Derived extends Base {
        public int a = 3;
        public int d = 4;
    
        public void test() {
            System.out.println(a); // 访问自己的a
            System.out.println(super.a); // 访问父类的a
        }
    }
    
    public class TestDemo2 {
        public static void main(String[] args) {
            new Derived().test();
        }
    }
    

    输出

    3
    1
    

super

  • superthis 一样,不能在静态方法中使用

  • super() 用来调用父类的构造方法,和 this() 一样,必须放在构造方法的第一行。用户不写 super(),编译器也会隐式地调用父类的默认构造,但是 this() 用户不写则没有;这说明:子类的构造,是先构造父类部分,然后再构造自己特有的部分

  • 编译器给子类中生成的默认构造相当于:

    class B extends A {
        B() {
            super();
        }
    }
    

注意

  • 父类中的 private 成员,在子类中无法访问,并不是因为没有继承这个成员,只是受到了访问限定符的限制

  • 如果父类和子类中都写了各自的静态代码块,实例代码块,构造方法。那么构造子类时的执行顺序是:

    父类的静态 子类的静态 父类的实例 父类的构造 子类的实例 子类的构造

    总结一下就是,首先静态优先,其次父类优先,最后实例优先

final

  • final 修饰的类叫做 密封类,不可被继承。
  • final 修饰的变量叫常量,不可被修改,注意:final 修饰数组时,表示这个数组引用的指向不可变,并不是表示数组里的内容不可变
  • final 修饰方法,表示该方法不可被重写

多态

向上转型

父类的引用可以指向子类的对象,这叫做向上转型,达成向上转型的方式有:直接赋值,方法转参和方法的返回值

向上转型后的父类引用依然只能访问父类自己的成员

重写

子类与父类中的两个方法,如果返回类型相同,方法名相同,参数列表相同,但实现的方法体不同,则这两个方法构成重写,称子类重写了父类的方法。

重写的方法上面加 @Override 注解,可以用来帮助我们进行合法性校验

构成重写的两个方法的方法名和参数列表都相同,当向上转型后的父类引用要调用时,实际上调用的是子类重写的那个,而不会调用父类原本的方法。

注意:

  • 静态方法、private 方法、被 final 修饰的方法 不能进行重写
  • 重写的子类方法的访问修饰符限定范围要大于等于父类
  • 协变:即使返回类型不同,也有可能构成重写:当这两个方法的返回值也是父子关系,且父类中的被重写的方法的返回值是父,子类中进行重写的方法的返回值是子,那么也构成重写,也叫协变关系。

多态

通过父类引用调用重写的方法,在代码运行时,传递不同类的子类对象可以调用不同的方法,这就是多态的体现

class Animal {
    public void eat() {
        System.out.println("进食");
    }
}

class Dog extends Animal {
    @Override
    public void eat() {
        System.out.println("吃狗粮");
    }
}

class Cat extends Animal {
    @Override
    public void eat() {
        System.out.println("吃猫粮");
    }
}

public class TestDemo {

    static void function(Animal animal) {
        animal.eat(); // 同样一个语句,却输出了两种结果
    }
    public static void main(String[] args) {
        function(new Dog());
        function(new Cat());
    }
}

输出

吃狗粮
吃猫粮

注:

静态绑定:也称前期绑定(早绑定),即在编译时,根据用户所传递实参类型就确定了具体调用的方法。典型代表是方法重载

动态绑定:也称后期绑定(晚绑定),即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用哪个类的方法

向下转型

Java 也可以向下转型,但用得比较少,而且不安全

class Animal {
    public void eat() {
        System.out.println("进食");
    }
}

class Dog extends Animal {
    @Override
    public void eat() {
        System.out.println("吃狗粮");
    }
}

class Cat extends Animal {
    @Override
    public void eat() {
        System.out.println("吃猫粮");
    }
}

class Bird extends Animal {
    public void fly() {
        System.out.println("正在飞");
    }
}

public class TestDemo {
    public static void main(String[] args) {
        Animal animal = new Bird();
        Bird bird = (Bird) animal; // 向下转型,将父类转回子类,需要强制类型转换
        bird.fly();
    }
}

输出:

正在飞

不安全在哪里呢?如果你这样写:

public class TestDemo {
    public static void main(String[] args) {
        Animal animal = new Dog(); // new 的是 Dog 类
        Bird bird = (Bird) animal;
        bird.fly();
    }
}

编译通过,但是运行时会抛 ClassCastException 异常,狗转成鸟,这不合理,当然不能强制转换。

为了提高向下转型的安全性,可以使用 instanceof 判断一个对象是不是一个类的实例:

public class TestDemo {
    public static void main(String[] args) {
        Animal animal = new Dog();
        if (animal instanceof Bird) { // 判断animal指向的对象是不是Bird类的实例,这里不是,返回false
            Bird bird = (Bird) animal;
            bird.fly();
        }
    }
}

多态的优点案例

public static void drawShapes() {
    Rect rect = new Rect();
    Cycle cycle = new Cycle();
    Flower flower = new Flower();
    String[] shapes = {"cycle", "rect", "cycle", "rect", "flower"};
    for (String shape : shapes) {
        if (shape.equals("rect")) {
            rect.draw();
        } else if (shape.equals("cycle")) {
            cycle.draw();
        } else if (shape.equals("flower")){
            flower.draw();
        }
    }
}

可改写为

public static void drawShapes() {
    Rect rect = new Rect();
    Cycle cycle = new Cycle();
    Flower flower = new Flower();
    Shape[] shapes = {cycle, rect, cycle, rect, flower};
    for (Shape shape : shapes) {
        shape.draw();
    }
}

多态的坑

class B {
    public B() {
        func();
    }

    public void func() {
        System.out.println("B.func()");
    }
}

class D extends B {
    @Override
    public void func() {
        System.out.println("D.func()");
    }
}

public class Test {
    public static void main(String[] args) {
        new D();
    }
}

输出:

D.func()

解析:new D() 会调用 D 的构造方法,D 的构造方法会先调用 B 的构造方法,B 的构造方法调用 func() ,打印了 D.func()。这说明它调用的是 Dfunc() 而不是 B 它自己的。为什么?因为这里触发了动态绑定,因为子类重写了就去调用子类的了(即使子类对象还没构造完成)

所以,尽量不要在构造方法里调用其他成员方法

抽象类和接口

抽象类

在面向对象的概念中,所有的对象都是由类来描绘的,但反过来,并不是所有的类都是用来描绘对象的。当一个类不能完整地描述一个对象时,这个类就是抽象类

抽象类使用 abstract 关键字修饰,abstract 修饰的方法,就是抽象方法

  • 抽象类不能被实例化;普通类能够定义的成员,抽象类也可以定义
  • 抽象类存在的最大意义就是为了被继承,被继承的抽象类也是父类,可以发生向上转型、多态
  • finalabstract 关键字不能共存,抽象方法也不能是 private 访问权限
  • 当一个普通的类,继承了抽象类之后,那么这个普通类必须重写抽象类中所有的抽象方法
  • 当一个抽象类 A,继承了抽象类 B,那么 A 可以不重写 B 的抽象方法,如果有一个普通类继承了 A,那么它必须重写所有的抽象方法
  • 抽象类不一定有抽象方法,抽象方法所在的类一定是抽象类

接口

Java 中,接口可以看成是多个类的公共规范,是一种引用数据类型,接口不是类,但可以看成一种特殊的类

接口定义

接口使用 interface 关键字来定义

  • 接口中的方法只能是抽象方法。所有的方法,默认都是 public abstract 修饰
  • 接口中的成员变量,默认都是 public static final 修饰
  • 接口中使用 default 来修饰的方法叫做默认方法,你可以在接口中实现默认方法,后续的实现类可以选择性地重写默认方法
  • 接口中的静态方法,也可以有具体的实现
interface IShape {
    // 成员变量
    int a = 10; // public static final 修饰的成员变量
    // 成员方法
    // 默认方法,有实现
    default void func() {
        System.out.println("默认的方法");
    }
	// 静态方法,有实现
    public static void staticFunc() { 
        System.out.println("静态方法");
    }

    void draw(); // public abstract 修饰的方法
}
  • 接口不能进行实例化,那么当然也不能有代码块和构造方法

实现接口

  • 一个普通类,可以通过 implements 来实现接口,且必须实现所有的抽象方法,否则你只能把它定义为抽象类

例:

// USB接口
interface USB {
    void openDevice();
    void closeDevice();
}

//鼠标类,实现USB接口
class Mouse implements USB {
    @Override
    public void openDevice() {
        System.out.println("打开鼠标");
    }

    @Override
    public void closeDevice() {
        System.out.println("关闭鼠标");
    }

    public void click() {
        System.out.println("鼠标点击");
    }
}

class KeyBoard implements USB {
    @Override
    public void openDevice() {
        System.out.println("打开键盘");
    }

    @Override
    public void closeDevice() {
        System.out.println("关闭键盘");
    }

    public void inPut() {
        System.out.println("键盘输入");
    }
}

class Computer {
    public void powerOn() {
        System.out.println("打开电脑");
    }

    public void powerOff() {
        System.out.println("关闭电脑");
    }

    public void useDevice(USB usb) {
        usb.openDevice();
        if (usb instanceof Mouse) {
            Mouse mouse = (Mouse) usb;
            mouse.click();
        } else if (usb instanceof KeyBoard) {
            KeyBoard keyBoard = (KeyBoard) usb;
            keyBoard.inPut();
        }
        usb.closeDevice();
    }

}

public class Test {
    public static void main(String[] args) {
        Computer computer = new Computer();
        computer.powerOn();
        // 使用鼠标设备
        computer.useDevice(new Mouse());
        // 使用键盘设备
        computer.useDevice(new KeyBoard());

        computer.powerOff();
    }
}

输出:

打开电脑
打开鼠标
鼠标点击
关闭鼠标
打开键盘
键盘输入
关闭键盘
关闭电脑
  • 接口虽然不是类,但是编译完的字节码文件也是 .class 后缀

一个类实现多个接口

一个类可以实现多个接口,这是 Java 对单继承的一个补充

class Animal {
    public String name;
    public int age;
    public void eat() {
        System.out.println("进食");
    }
}

interface IFlying {
    void fly();
}

interface IRunning {
    void run();
}

interface ISwimming {
    void swim();
}

// 狗类实现跑和游泳两个接口
class Dog extends Animal implements IRunning, ISwimming {
    @Override
    public void run() {
        System.out.println(this.name + " 正在跑");
    }

    @Override
    public void swim() {
        System.out.println(this.name + "正在游泳");
    }
}

接口也可以发生向上转型,实现多态

package demo1;

class Animal {
    public Animal() {}
    public Animal(String name) {
        this.name = name;
    }
    public String name;
    public void eat() {
        System.out.println("进食");
    }
}

interface IFlying {
    void fly();
}

interface IRunning {
    void run();
}

interface ISwimming {
    void swim();
}

// 狗类实现跑和游泳两个接口, 表示狗是动物,具备跑和跳的能力
class Dog extends Animal implements IRunning, ISwimming {
    public Dog(String name) {
        super(name);
    }
    @Override
    public void run() {
        System.out.println(this.name + " 正在跑");
    }

    @Override
    public void swim() {
        System.out.println(this.name + "正在游泳");
    }
}

// 鸟是动物,能飞
class Bird extends Animal implements IFlying {
    public Bird(String name) {
        super(name);
    }

    @Override
    public void fly() {
        System.out.println(this.name + " 正在飞");
    }
}

// 机器人不是动物,能跑、游泳、飞
class Robot implements IRunning, ISwimming, IFlying {
    @Override
    public void fly() {
        System.out.println("机器人正在飞");
    }

    @Override
    public void run() {
        System.out.println("机器人正在跑");
    }

    @Override
    public void swim() {
        System.out.println("机器人 正在游泳");
    }
}

public class Test {
    // 具备飞能力的就可以调用这个方法
    static void func1(IFlying iFlying) {
        iFlying.fly();
    }

    // 具备游泳能力的可以调用这个方法
    static void func2(ISwimming iSwimming) {
        iSwimming.swim();
    }

    // 具备跑能力的可以调用这个方法
    static void func3(IRunning iRunning) {
        iRunning.run();
    }

    // 动物可以调用这个方法
    static void func4(Animal animal) {
        animal.eat();
    }
    public static void main(String[] args) {
        Dog dog = new Dog("旺旺");
        Bird bird = new Bird("唧唧");
        Robot robot = new Robot();
        func1(bird);
        func2(robot);
        func3(dog);
    }
}

接口继承

接口之间可以用 extends 来继承

interface A {
    void funcA();
}

interface B {
    void funcB();
}

// C 拓展了 A、B
interface C extends A, B {
    void funcC();
}

// 实现C的普通类,要重写所有抽象方法
class AA implements C {
    @Override
    public void funcA() {
        
    }

    @Override
    public void funcB() {

    }

    @Override
    public void funcC() {

    }
}

常用接口

Comparable

下面介绍一个常用接口,ComparableT 为模板参数

一个类通过重写这个接口中的 compareTo() 方法,来实现对这个类的对象的比较。

// 实现 Comparable 接口
class Student implements Comparable<Student> {
    public String name;
    public int age;
    public double score;

    public Student(String name, int age, double score) {
        this.name = name;
        this.age = age;
        this.score = score;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", score=" + score +
                '}';
    }

    // 重写接口中的 compareTo() 方法,指定按 age 比较
    @Override
    public int compareTo(Student o) {
        return this.age - o.age;
    }
}

然后,我们就可以使用 Arrays.sort() 对 Student 对象按 age 进行排序

public class Test {
    public static void main(String[] args) {
        Student[] students = new Student[3];
        students[0] = new Student("zhangsan", 66, 78.9);
        students[1] = new Student("lisi", 32, 96.4);
        students[2] = new Student("wangwu", 100, 25.5);

        Arrays.sort(students);

        System.out.println(Arrays.toString(students));
    }
}

输出

[Student{name='李四', age=32, score=96.4}, Student{name='张三', age=66, score=78.9}, Student{name='王五', age=100, score=25.5}]

return this.age - o.age; 改为 return o.age - this.age; 则按降序排序

  • Java 不支持运算符重载,如果使用小于号 <,比较的只是两个对象的引用

  • 我们所熟知的 String 数据类型也实现了 Comparable,所以我们可以使用 compareTo() 来进行字符串比较

    String str1 = "apple";
    String str2 = "banana";
    
    // 使用 compareTo 方法比较字符串
    int result = str1.compareTo(str2);
    // result 的值为负数,因为 "apple" 在字典顺序中小于 "banana"
    
Comparator

类似于 C++ 中的仿函数,我们可以在 Student 类外,来指定比较方法

class Student {
    public String name;
    public int age;
    public double score;

    public Student(String name, int age, double score) {
        this.name = name;
        this.age = age;
        this.score = score;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", score=" + score +
                '}';
    }
}

// 实现了Comparator接口的类
class AgeComparator implements Comparator<Student> {
    @Override
    public int compare(Student o1, Student o2) {
        return o1.age - o2.age;
    }
}

public class Test {
    public static void main(String[] args) {
        Student[] students = new Student[3];
        students[0] = new Student("zhangsan", 66, 78.9);
        students[1] = new Student("lisi", 32, 96.4);
        students[2] = new Student("wangwu", 100, 25.5);

        Arrays.sort(students, new AgeComparator()); // 传入第二个参数,指定比较方式

        System.out.println(Arrays.toString(students));
    }
}
Cloneable接口和深拷贝

Cloneable 是一个空接口,又叫标记接口,里面没有方法。

一个类实现 Cloneable 表示该类是可以被克隆的。

然后,我们需要重写从 Object 类继承下来的 clone() 方法

package demo3;


class Money {
    public double money = 99.9;
}

// 实现 Cloneable 接口,表示该类的对象是可以被克隆的
class Person implements Cloneable {
    public String id = "1234";
    public Money m = new Money();

    @Override
    public String toString() {
        return "Person{" +
                "id='" + id + '\'' +
                ", money=" + m +
                '}';
    }

    // 重写clone方法,返回值可以是任意引用类型,因为原clone方法返回类型是Object,这里利用了协变
    @Override
    public Person clone() {
        try {
            Person clone = (Person) super.clone();
            // TODO: copy mutable state here, so the clone can't change the internals of the original
            return clone;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
}

public class Test {
    public static void main(String[] args) {
        Person person1 = new Person();
        Person person2 = person1.clone(); // 产生一个副本

        // 改变 person2 的 m.money
        person2.m.money = 100;
        // 打印两个 Person 的 m.money 会发现都变成 100 了
        System.out.println(person1.m.money);
        System.out.println(person2.m.money);
        // 打印两个 Person 的 m 引用,发现一样
        System.out.println(person1.m);
        System.out.println(person2.m);
    }
}

输出:

100.0
100.0
demo3.Money@1b6d3586
demo3.Money@1b6d3586

但是这只是一个浅拷贝,并没有创建一个新的 Money 对象,只是多个一个引用而已,所以两个 Person 对象会共享一个 money

注意到 IDEA 在自动生成的 clone() 方法里面贴心地写了个 TODO 注释,让我们稍后在这个地方完成深拷贝。

具体步骤,在克隆的时候,将 m 也克隆一次,然后赋给新对象的 m 引用,为了使 m 可以克隆,需要让 Money 类实现 Cloneable 接口,并重写 clone() 方法

// 将 Money 也标记为可克隆的,并重写clone()方法
class Money implements Cloneable{
    public double money = 99.9;

    @Override
    public Money clone() {
        try {
            return (Money) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
}

// 实现 Cloneable 接口,表示该类的对象是可以被克隆的
class Person implements Cloneable {
    public String id = "1234";
    public Money m = new Money();

    @Override
    public String toString() {
        return "Person{" +
                "id='" + id + '\'' +
                ", money=" + m +
                '}';
    }

    // 重写clone方法
    @Override
    public Person clone() {
        try {
            Person clone = (Person) super.clone();
            clone.m = this.m.clone(); // 将 m 也克隆一份,传给新对象里的 m
            return clone;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
}

public class Test {
    public static void main(String[] args) {
        Person person1 = new Person();
        Person person2 = person1.clone(); // 产生一个副本

        // 改变 person2 的 m.money
        person2.m.money = 100;
        // 打印两个 Person 的 m.money 会发现不一样了,深拷贝成功
        System.out.println(person1.m.money);
        System.out.println(person2.m.money);
    }
}

输出

99.9
100.0

Object 类

Object 类是所有类的父类,所以 Object 类型的引用可能指向任意引用类型的对象

一个存了不同类型的对象的数组:

public class Test {
    public static void main(String[] args) {
        // 创建 Object 数组
        Object[] objectArray = new Object[3];

        // 存储不同类型的对象
        objectArray[0] = "Hello";
        objectArray[1] = 42;
        objectArray[2] = new SomeObject();

        // 访问数组中的对象
        for (Object obj : objectArray) {
            System.out.println(obj);
        }
    }
}

class SomeObject {
    // 这是一个简单的自定义类
}

输出:

Hello
42
demo3.SomeObject@1b6d3586

可以存 int 是因为这里发生了自动装箱,int 被转成 Integer 类了。

下面简单介绍 Object 类的几个常用方法

获取对象信息

toString()Object 类中的实现:

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

我们可以根据自己的需要去重写这个方法

equals()方法

equals()Object 类中的实现:

public boolean equals(Object obj) {
    return (this == obj);
}

这个实现和直接用 == 没区别,都是比较引用,要比较对象内的数据,需要我们自己去重写这个方法。

hashcode()方法

这是一个本地方法,用来计算一个对象的 hash 值

String 类

  • Java 中,没有 \0 是字符串结尾的说法

常用操作

字符串构造

// 使用常量字符串构造
String s1 = "Hello";
// 使用 new 构造
String s2 = new String("Hello");
// 使用字符数组构造
char[] array = {'a', 'b', 'c'};
String s3 = new String(array);

查看 String 的实现,其实用的是字符数组来存储数据

跨越编程界限:C++到JavaSE的平滑过渡_第4张图片

字符串比较

// 比较两个字符串的引用
s1 == s2;
// 比较两个字符串是否相同
s1.equals(s2);
// 比较两个字符串的大小
s1.compareTo(s2);
// 忽视大小写比较是否相同
s1.equalsIgnoreCase(s2);

字符串查找

方法 功能
char charAt(int index) 返回下标 index 处的字符
int indexOf(int ch) 返回 ch 第一次出现的位置,没有返回-1
int indexOf(int ch, int fromIndex) 从 fromIndex 位置开始找 ch 第一次出现的位置,没有返回-1
int indexOf(String str) 返回 str 第一次出现的位置,没有返回-1
int indexOf(String str, int fromIndex) 从 fromIndex 位置开始找 str 第一次出现的位置,没有返回-1
int lastIndex() 从后往前找,提供的重载和上面相同

转化

数值字符串转化

// 1.字符串转整数
String str1 = "123";
int intValue1 = Integer.parseInt(str1);
    // 或使用 valueOf 方法,该方法会将返回值装箱
Integer intValue2 = Integer.valueOf(str1);
// 2.字符串转浮点数
String str2 = "123.45";
float floatValue1 = Float.parseFloat(str2);
    // 或使用 valueOf 方法,该方法会将返回值装箱
Float floatValue2 = Float.valueOf(str2);
// 3.整数转字符串
int intValue3 = 123;
String str3 = String.valueOf(intValue3);
    // 或者使用 Integer 类的 toString 方法:
String str4 = Integer.toString(intValue3);
// 4.浮点数转字符串
float floatValue3 = 123.45f;
String str5 = String.valueOf(floatValue3);
    // 或者使用 Float 类的 toString 方法:
String str6 = Float.toString(floatValue3);

大小写转换

// 1.将字符串转换为大写
String originalString = "Hello, World!";
String upperCaseString = originalString.toUpperCase();
System.out.println(upperCaseString);
// 输出: HELLO, WORLD!

// 2.将字符串转换为小写
String lowerCaseString = originalString.toLowerCase();
System.out.println(lowerCaseString);
// 输出: hello, world!

// 3.将字符转换为大写
char originalChar1 = 'a';
char upperCaseChar = Character.toUpperCase(originalChar1);
System.out.println(upperCaseChar);
// 输出: A

// 4.将字符转换为小写
char originalChar2 = 'A';
char lowerCaseChar = Character.toLowerCase(originalChar2);
System.out.println(lowerCaseChar);
// 输出: a

注意,这些方法返回新的字符串或字符,而不是修改原始的字符串或字符。

字符串与数组间的转换

// 1.字符串转换为字符数组
String str1 = "Hello, World!";
char[] charArray1 = str1.toCharArray();
System.out.println(Arrays.toString(charArray1));
// 输出: [H, e, l, l, o, ,,  , W, o, r, l, d, !]

// 2.字符串转换为字节数组(使用默认字符集)
byte[] byteArray = str1.getBytes();
System.out.println(Arrays.toString(byteArray));
// 输出: [72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33]

// 3.字节数组转换为字符串(使用默认字符集)
String str2 = new String(byteArray);
System.out.println(str2);
// 输出: Hello, World!

// 4.字符数组转换为字符串
char[] charArray2 = {'H', 'e', 'l', 'l', 'o'};
String str = new String(charArray2);
System.out.println(str);
// 输出: Hello

格式化

// 1.基本格式化
String name = "Alice";
int age = 30;
String formattedString1 = String.format("Name: %s, Age: %d", name, age);
System.out.println(formattedString1);
// 输出: Name: Alice, Age: 30

// 2.指定小数点后的位数
double pi = Math.PI;
String formattedString2 = String.format("Value of pi: %.2f", pi);
System.out.println(formattedString2);
// 输出: Value of pi: 3.14

// 3.日期格式化
Date currentDate = new Date();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
String formattedDate = dateFormat.format(currentDate);
System.out.println(formattedDate);
// 输出类似: 2023-11-12

// 4.左对齐、宽度补齐
String text = "Java";
String formattedString3 = String.format("|%10s|", text);
System.out.println(formattedString3);
// 输出: |      Java|

// 5.使用位置参数
String firstName = "John";
String lastName = "Doe";
String formattedString4 = String.format("Last Name: %2$s, First Name: %1$s", firstName, lastName);
System.out.println(formattedString4);
// 输出: Last Name: Doe, First Name: John

替换

String originalString = "Hello, World!";
String replacedString = originalString.replace("World", "Java");

System.out.println("Original String: " + originalString);
System.out.println("Replaced String: " + replacedString);
/*输出:
Original String: Hello, World!
Replaced String: Hello, Java!
*/

分割字符串

String str = "abc def ghi";
String[] strings = str.split(" "); // 按空格分割
for (String s : strings) {
    System.out.println(s);
}
/*输出:
abc
def
ghi
*/
  • 如果分割符是 . | * + \,需要加上转义字符,比如要表示 . ,就应该写成 \\. 要表示 \ 应该写成 \\\\
  • 如果一个字符串中有多个分隔符,可以用 | 作为连字符

字符串截取

String str = "abcdefghi";
String ret = str.substring(2, 7); // 截取下标 [2, 7) 范围的字符串
System.out.println(ret);
// 输出:cdefg

去掉左右空格

String str = "  Hello world   ";
String ret = str.trim();
System.out.println(ret);
//输出:Hello world

字符串常量池

String str1 = "hello";
String str2 = "hello";
String str3 = new String("hello");
System.out.println(str1 == str2); // true
System.out.println(str1 == str3); // false

为什么 str1str2 引用的是同一个,而 str3 不是呢?这就和字符串常量池有关了

  • 字符串常量池就是 StringTable,存储在堆区,StringTable 是一个哈希表,它的键是字符串的内容,而值是对字符串对象的引用。
  • 只要是双引号引起来的元素,全都存放在常量池中,且只有一份
  • 当你使用双引号创建字符串常量时,Java 会尝试在字符串池中查找是否已经存在相同内容的字符串,如果存在,就返回对该字符串的引用,如果不存在,就在字符串池中创建一个新的字符串并返回引用。

上述代码的存储结构:

跨越编程界限:C++到JavaSE的平滑过渡_第5张图片

创建 s1 的时候,从 StringTable 中找 “hello”,没找到,创建 String 对象,存入 StringTable,返回引用给 s1

创建 s2 的时候,从 StringTable 中找 “hello”,找到了,所以直接返回 String 对象的引用,所以 s1 引用和 s2 引用相同

创建 S3 ,因为是显式地 new 出来,所以会创建一个新的 String 对象并返回引用,然后从 StringTable 中找“hello”,找到后返回引用给到 String 对象里的 value 数组引用。所以 s3 和 s1、s2 不同,但 s3 指向的对象里的 value 和它们是相同的

总结:只要 new 的对象,返回的引用都是唯一的;使用常量字符串创建 String 对象的效率更高,而且更节省空间。

特殊案例:

String str1 = "hello";
String str2 = "he" + "llo"; // 因为是常量,在编译的时候就被当做 "hello" 了
System.out.println(str1 == str2); // true
String str1 = "hello";
String str2 = "he";
String str3 = "llo";
String str4 = str2 + str3; // 因为是变量,在编译的时候,还不知道结果是什么
System.out.println(str1 == str4); // false

str2 + str3 是在运行时拼起来的,而不是通过 "" 创建的,所以它的结果不会放在常量池,返回的引用也就不一样

intern 方法

intern 是一个 native 方法,该方法的作用是手动将创建的 String 对象添加到常量池中

char[] ch = {'h', 'e', 'l', 'l', 'o'};
String str1 = new String(ch);
str1.intern();
String str2 = "hello";
System.out.println(str1 == str2); // true

第 2 行,由于不是常量字符串,“hello” 并不会加入常量池

第 3 行,使用 intern 手动加入常量池,会将 <"hello", str1> 这个键值对存入 StringTable。

第 4 行,从常量池中找 “hello”,找到,直接返回 String 对象的引用,也就是第 2 行创建的那个引用

所以最后的 str1 和 str2 相等。

字符串不可变

  • String 类中的字符实际上是由一个 value 字符数组存储,这个数组由 private final 修饰,表示这是私有成员,我们无法访问,value 的指向也不能发生改变。
  • String 类提供的修改操作都不是原地修改,而是返回了一个新的 String 对象

这意味着,String 对象是不可变(immutable)的,一旦创建了 String 对象,它的内容就不能被修改。

String str = "Hello";
str = str + " World"; // 创建了一个新的字符串对象给str引用,而不是修改原始字符串,原字符串对象将被回收

为什么 String 要设计成不可变的?

  • 方便实现字符串对象池,如果 String 可变,那么对象池就需要考虑写时拷贝的问题
  • 不可变对象是线程安全的
  • 不可变对象更方便缓存 hashcode,作为 key 时可以更高效地保存到 HashMap 中。

既然 String 是不可变,那么如果我写出这样的代码:

public static void main(String[] args) {
    String str = "hello";
    for (int i = 0; i < 10; ++i) {
        str += i;
    }
}

这段代码在循环中使用 += 拼接字符串,会产生大量的临时对象,性能是非常低的。

StringBuilder和StringBuffer

对于需要频繁修改的字符串,建议使用 StringBuilder

public static void main(String[] args) {
    StringBuilder str = new StringBuilder("hello");
    for (int i = 0; i < 10; ++i) {
        str.append(i);
    }
    System.out.println(str);
}

StringBuilderStringBuffer 类是可变的字符串类。这两个类允许你进行高效的字符串操作,而不会频繁创建新的对象。在这两者之间,StringBuilder 是非线程安全的,而 StringBuffer 是线程安全的,但在单线程环境中,通常建议使用 StringBuilder,因为它的性能更好。

逆置字符串

reverse() 方法是 String 中没有的,StringBuilderStringBuffer 中有,并且是原地修改

StringBuffer str = new StringBuffer("hello");
str.reverse();
System.out.println(str);
// 输出:olleh

拼接字符串

public static void main(String[] args) {
    StringBuilder stringBuilder = new StringBuilder();

    // 追加字符串
    stringBuilder.append("Hello");
    stringBuilder.append(" World");

    System.out.println(stringBuilder); // 输出 "Hello World"
}

面试题:

  1. StringStringBufferStringBuilder 的区别

    • String 的内容不可修改,StringBufferStringBuilder 的内容可以修改
    • StringBufferStringBuilder 大部分功能是相似的
    • StringBuffer 采用同步处理,属于线程安全操作;而 StringBuilder 未采用同步处理,属于线程不安全操作
  2. 以下代码创建了几个对象

    String str = new String("a");
    

    创建了两个对象,字符串常量池中的对象和强制 new 出来的堆中的对象。这种做法通常是不推荐的

    String str = new String("a") + new String("b");
    

    创建了6个对象,字符串常量池中的1个“a”对象,1个“b”对象,堆中的1个“a”对象,1个“b”对象,一个拼接的StringBuilder对象“ab”,为了赋给str转为String的对象“ab”

异常

Java 异常结构图

跨越编程界限:C++到JavaSE的平滑过渡_第6张图片

  1. Throwable:异常体系的顶层类,其派生出两个重要的子类,Error 和 Exception
  2. Error:指 JVM 无法解决的严重问题,如 JVM 内部错误,资源耗尽等。典型代表:StackOverflowError 和 OutOfMemoryError
  3. Exception:异常产生后程序可以通过代码进行处理,使程序继续执行

异常的分类

受查异常(编译时异常)

在程序编译期间发生的异常,称为受查异常,如 clone 方法,会抛出此异常。上图蓝色部分即受查异常

非受查异常(运行时异常)

在程序运行期间发生的异常,称为非受查异常,如空指针异常,数组越界异常。上图红色部分即非受查异常

异常的处理

防御式编程

LBYL:Look Before You Leap,每一步操作都进行检查

事后认错型

EAFP:It’s Easier to Ask Forgiveness than Permission,先把操作都放在一块,遇到了问题再处理

public static void main(String[] args) {
    int[] array = {1, 2, 3, 4, 5};
    try {
        System.out.println(array[9]);
    } catch (ArrayIndexOutOfBoundsException e) {
        System.out.println("捕获到一个数组越界的异常!");
        e.printStackTrace();
    }
}
/*输出:
捕获到一个数组越界的异常!
java.lang.ArrayIndexOutOfBoundsException: 9
	at demo3.Test.main(Test.java:7)
*/

异常也可以抛出,但如果抛给 JVM 了,程序就会异常终止。

static void func() {
    throw new NullPointerException();
}
public static void main(String[] args) {
    int[] array = {1, 2, 3, 4, 5};
    try {
//            System.out.println(array[9]);
        func();
    } catch (ArrayIndexOutOfBoundsException e) {
        System.out.println("捕获到一个数组越界异常!");
        e.printStackTrace();
    } catch (NullPointerException e) {
        System.out.println("捕获到一个空指针异常!");
        e.printStackTrace();
    }
}

多个 catch 块,分别处理异常

static void func() {
    throw new NullPointerException();
}
public static void main(String[] args) {
    int[] array = {1, 2, 3, 4, 5};
    try {
//            System.out.println(array[9]);
        func();
    } catch (ArrayIndexOutOfBoundsException | NullPointerException e) { // 一条catch可以捕获多种异常,用 | 分隔
        System.out.println("捕获到一个数组越界异常或空指针异常!");
        e.printStackTrace();
    }
}

一条catch可以捕获多种异常,用 | 分隔。

使用 throws 可以声明一个方法会抛异常,如果有多种异常会抛出,可以用 , 分隔

static void func() throws NullPointerException{
    throw new NullPointerException();
}

注意:

  • throw 必须写在方法体内部
  • 抛出的对象必须是 Exception 类或者其子类的对象
  • 运行时异常可以不用处理,交给 JVM 来处理,异常终止程序
  • 如果你使用了一个可能抛出编译时异常的方法,那么要么使用 try-catch 块捕获这个异常,要么在方法签名中使用 throws 关键字声明这个异常,否则无法通过编译。

finally

catch 的最后,还可以写一个 finally 块。

不管异常是否发生,finally 都会在方法结束的前执行。

finally 常用来关闭资源

public static void main(String[] args) {
    Scanner scanner = new Scanner(System.in);
    try {
        System.out.println("一些代码");
    } catch (Exception e) {
        System.out.println("一些异常处理");
    } finally {
        scanner.close();
    }
}

特殊案例:

public static int func() {
    try {
        return 10;
    } finally {
        return -1;
    }
}
public static void main(String[] args) {
    System.out.println(func());
}
// 输出:-1

因为 finally 是一定会被执行的,所以就算 try 里面 return 了,方法也不会结束,而是继续执行 finally 的语句,所以返回值被 -1 覆盖了

注:尽量不要在 finally 里使用 return

异常处理流程

  • 程序先执行 try 中的代码
  • 如果 try 中的代码出现异常,就会结束 try 中的代码,看 catch 中的异常类型是否匹配
  • 如果找到匹配的异常类型,就会执行 catch 中的代码,执行完 catch 块,程序继续运行 catch 块后面的代码
  • 如果没有找到匹配的异常类型,就会将异常向上传递给上层调用者
  • 无论是否找到匹配的异常类型,finally 块都会被执行(在该方法结束前)
  • 如果上层调用者也没有处理异常,就继续向上传递
  • 直到 main 方法也没有处理异常,就会交给 JVM 来处理,此时程序就会异常终止

自定义异常

如果要自定义一个异常,那么需要继承一个异常类,一般建议继承这2个中的一个:

  1. Exception 【默认是一个编译时异常】
  2. RuntimeException【默认是一个运行时异常】
public class UserNameErrorException extends RuntimeException{
    public UserNameErrorException() {
    }

    public UserNameErrorException(String message) {
        super(message);
    }
}

public class PasswordErrorException extends RuntimeException {
    public PasswordErrorException() {
    }

    public PasswordErrorException(String message) {
        super(message);
    }
}

class Login {
    private String userName = "admin";
    private String password = "123456";
    public void loginInfo(String userName, String password) throws UserNameErrorException {
        if (!this.userName.equals(userName)) {
            throw new UserNameErrorException("这是一个用户名异常的参数");
        }
        if (!this.password.equals(password)) {
            throw new PasswordErrorException("这是一个密码异常的参数");
        }
    }
}

public class TestDemo {
    public static void main(String[] args) {
        try {
            new Login().loginInfo("admin1", "12345");
        } catch (UserNameErrorException | PasswordErrorException e) {
            e.printStackTrace();
        }
    }
}

/*输出:
    Exception in thread "main" demo4.UserNameErrorException: 这是一个用户名异常的参数
	at demo4.Login.loginInfo(TestDemo.java:8)
	at demo4.TestDemo.main(TestDemo.java:18)
*/

你可能感兴趣的:(Java,开发语言,java,intellij-idea,jvm)