安装JDK
配置环境变量:
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
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
解释:
public
访问修饰限定符class
定义类的关键字HelloWorld
类名,建议使用大驼峰如果一个类是 public 修饰的,那么这个类的类名要和文件名一样。
main 函数固定写法 public static void main(String[] args)
System.out.println("Hello world");
另外两个打印函数:
System.out.print("Hello world");
不带ln,区别在于不换行
System.out.printf("%s", "Hello world");
格式化输出,同C语言
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快捷键:
psvm
或 main
:生成 main 函数。sout
:生成 println 函数,也可以写一个东西然后加个.sout基本数据类型:
byte
short
int
long
float
double
boolean
char
引用数据类型:
String
,类,接口,枚举注意:
int
占 4 字节,long
占 8 字节,short
占 2 字节,byte
是区别于 C 语言的一个新的类型,只占 1 个字节char
占 2
字节,所以 Java 中的 char
可以存中文字符打印int的最大值和最小值:
System.out.println(Integer.MAX_VALUE);
System.out.println(Integer.MIN_VALUE);
其中的 Integer
是包装类——基本数据类型所对应的类类型
int
的类类型是 Integer
,char
的类类型是 Character
,其他的基本数据类型对应的类类型都是首字母大写
字符串String之间使用 +
号进行拼接,其他类型,如 int
和 String
进行 +
运算也是拼接,而不会进行数字计算,例:
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)
注:
使用普通的 for/while 循环
Java 支持 for-each,和 C++11 的语法一样。
借助 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()
方法可以将数组转为字符串,而且还用 ,
和 []
加了格式。
for 循环
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));
}
}
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));
}
}
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));
}
}
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 通过方法签名来标识一个方法。
方法签名:经过编译器编译修改过的方法的最终名字,具体为:方法全路径名+参数列表+返回值类型。
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
指令反汇编:
其中,方法区和堆是由所有线程共享的,其他数据区是线程隔离的。
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都指向都指向了第二个对象,此时第一个对象没有被引用,会被垃圾回收器自动回收
小练习:
下面的程序输出什么?
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]
解析:形参的改变不会影响实参,虽然形参指向了另一个对象,但是实参指向的还是原来的对象,所以不变。
下面的程序输出什么?
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
软性要求
main
方法所在的类一般要使用 public
修饰public
修饰的类的名称,如要修改,要通过开发工具修改(IDEA 中右键文件名->Refactor->Rename File…)硬性要求
public
修饰的类必须要和文件名相同特性
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 构造一个对象的过程:
检测对象对应的类是否加载了,如果没有加载则加载
为对象分配内存空间
处理并发安全问题
比如:多个线程同时申请对象,JVM 要保证给对象分配的空间不冲突
初始化所分配的空间
即:对象空间被申请好之后,对象中包含的成员已经设置好了初始值(零值)
设置对象头信息
调用合适的构造方法
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.
的,静态导入后就不需要了。
自己创建包
com.cero.demo1
com.cero.demo1
的包,那么会存在一个对应的路径 com/cero/demo1
来存储代码我们可以创建出这样一个结构:
package
声明自己所在的包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();
}
常见的包
java.lang
系统常用基础类(String、Object),此包自 JDK1.1 后自动导入java.lang.reflect
java 反射编程包java.net
网络编程开发包java.sql
数据库开发包java.util
java 工具程序包 。(重要)java.io
I/O 编程开发包静态成员变量
特殊案例:
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
引用,不能访问非静态成员静态成员变量初始化
静态成员变量可以就地初始化,也可以通过代码块初始化。
使用 {}
定义的一段代码称为代码块,根据代码块定义的位置以及关键字,又可分为以下四种
普通代码块(本地代码块)
定义在方法中的代码块
public class TestDemo {
public static void main(String[] args) {
{
System.out.println("haha");
}
}
}
这种用法比较少。
实例代码块(构造代码块)
写在类中,优先于构造方法执行,类似于 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个参数的构造方法
如果一个类有多个实例代码块,则按照定义的顺序执行
静态代码块
一般用于初始化静态数据成员,比实例代码块更早执行
static {
classes = "计算机231";
System.out.println("静态代码块,一般用于初始化静态数据成员");
}
输出
静态代码块,一般用于初始化静态数据成员
实例代码块,一般用来初始化实例(普通)成员变量
调用带有3个参数的构造方法
注意:
同步代码块(用不到)
定义在一个类的内部的类叫做内部类。当一个事物的内部有一部分需要一个完整的结构来描述,那么这个完整结构可以使用内部类。
内部类经过编译会生成独立的字节码文件 外部类名$内部类名.class
内部类分为四种
实例内部类(成员内部类)
在外部需要使用 外部类名.内部类名 变量名 = 外部类对象的引用.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);
}
实例内部类所处的位置与外部类的成员是相同的,所以也可以用访问限定符修饰
静态内部类:被 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();
}
}
注意
new
一个外部类的对象出来局部内部类:
定义在方法中的内部类,几乎不用。
只能在所在方法内部使用;不能被访问限定符修饰,static
修饰
匿名内部类
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
super
和 this
一样,不能在静态方法中使用
super()
用来调用父类的构造方法,和 this()
一样,必须放在构造方法的第一行。用户不写 super()
,编译器也会隐式地调用父类的默认构造,但是 this()
用户不写则没有;这说明:子类的构造,是先构造父类部分,然后再构造自己特有的部分
编译器给子类中生成的默认构造相当于:
class B extends A {
B() {
super();
}
}
注意:
父类中的 private 成员,在子类中无法访问,并不是因为没有继承这个成员,只是受到了访问限定符的限制
如果父类和子类中都写了各自的静态代码块,实例代码块,构造方法。那么构造子类时的执行顺序是:
父类的静态 子类的静态 父类的实例 父类的构造 子类的实例 子类的构造
总结一下就是,首先静态优先,其次父类优先,最后实例优先
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()
。这说明它调用的是 D
的 func()
而不是 B
它自己的。为什么?因为这里触发了动态绑定,因为子类重写了就去调用子类的了(即使子类对象还没构造完成)
所以,尽量不要在构造方法里调用其他成员方法
在面向对象的概念中,所有的对象都是由类来描绘的,但反过来,并不是所有的类都是用来描绘对象的。当一个类不能完整地描述一个对象时,这个类就是抽象类
抽象类使用 abstract
关键字修饰,abstract
修饰的方法,就是抽象方法
final
和 abstract
关键字不能共存,抽象方法也不能是 private
访问权限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();
}
}
输出:
打开电脑
打开鼠标
鼠标点击
关闭鼠标
打开键盘
键盘输入
关闭键盘
关闭电脑
一个类可以实现多个接口,这是 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
下面介绍一个常用接口,Comparable
,T
为模板参数
一个类通过重写这个接口中的 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
表示该类是可以被克隆的。
然后,我们需要重写从 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
类型的引用可能指向任意引用类型的对象
一个存了不同类型的对象的数组:
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 值
\0
是字符串结尾的说法字符串构造
// 使用常量字符串构造
String s1 = "Hello";
// 使用 new 构造
String s2 = new String("Hello");
// 使用字符数组构造
char[] array = {'a', 'b', 'c'};
String s3 = new String(array);
查看 String
的实现,其实用的是字符数组来存储数据
字符串比较
// 比较两个字符串的引用
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
为什么 str1
和 str2
引用的是同一个,而 str3
不是呢?这就和字符串常量池有关了
上述代码的存储结构:
创建 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
可变,那么对象池就需要考虑写时拷贝的问题既然 String
是不可变,那么如果我写出这样的代码:
public static void main(String[] args) {
String str = "hello";
for (int i = 0; i < 10; ++i) {
str += i;
}
}
这段代码在循环中使用 +=
拼接字符串,会产生大量的临时对象,性能是非常低的。
对于需要频繁修改的字符串,建议使用 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);
}
StringBuilder
和 StringBuffer
类是可变的字符串类。这两个类允许你进行高效的字符串操作,而不会频繁创建新的对象。在这两者之间,StringBuilder
是非线程安全的,而 StringBuffer
是线程安全的,但在单线程环境中,通常建议使用 StringBuilder
,因为它的性能更好。
逆置字符串
reverse()
方法是 String
中没有的,StringBuilder
和 StringBuffer
中有,并且是原地修改
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"
}
面试题:
String
、StringBuffer
、StringBuilder
的区别
String
的内容不可修改,StringBuffer
与 StringBuilder
的内容可以修改StringBuffer
与 StringBuilder
大部分功能是相似的StringBuffer
采用同步处理,属于线程安全操作;而 StringBuilder
未采用同步处理,属于线程不安全操作以下代码创建了几个对象
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”
Throwable
:异常体系的顶层类,其派生出两个重要的子类,Error 和 ExceptionError
:指 JVM 无法解决的严重问题,如 JVM 内部错误,资源耗尽等。典型代表:StackOverflowError 和 OutOfMemoryErrorException
:异常产生后程序可以通过代码进行处理,使程序继续执行受查异常(编译时异常)
在程序编译期间发生的异常,称为受查异常,如 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
类或者其子类的对象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个中的一个:
Exception
【默认是一个编译时异常】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)
*/