一:抽象类
1.1 抽象类的引入
在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,所有的类都能够完整的描述一个对应的对象吗?很显然不是,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类,举个例子:
当我们描述一个动物的时候,我们说它会吃饭,会睡觉,会移动,很显然,符合这个特征的动物有很多,我们并不能具体的确定是哪一个动物,我们还需要对这个类进一步的描述,比如说怎么睡觉,怎么移动,怎么吃饭(也就相当于重写啦),因为 java 是一门面向对象的语言,所以 java 为我们提供了一个全新的语法: 抽象类
1.2 抽象方法
在 Java 中,抽象方法是一种没有具体实现的方法,只有方法签名而没有方法体。它用于提供某个行为的规范,让具体的子类去重写,以此让子类去重新实现。
抽象方法的声明使用关键字 abstract
,并且不能被 final或者 static 修饰。( 因为抽象方法要被重写,被 final 和 static 修饰就不能重写了。当然 private 也不行 )。抽象方法的存在就是为了让子类去实现,子类必须提供抽象方法的具体实现。
下面是一个简单的示例,以动物类为例:
// 抽象类 Animal
abstract class Animal {
// 抽象方法,用于定义动物的声音
public abstract void makeSound();
}
// 子类 Cat 继承自 Animal
class Cat extends Animal {
// 实现抽象方法
public void makeSound() {
System.out.println("喵喵喵");
}
}
// 子类 Dog 继承自 Animal
class Dog extends Animal {
// 实现抽象方法
public void makeSound() {
System.out.println("汪汪汪");
}
}
// 测试类
public class Main {
public static void main(String[] args) {
Animal cat = new Cat(); // 创建 Cat 对象
cat.makeSound(); // 打印出"喵喵喵"
Animal dog = new Dog(); // 创建 Dog 对象
dog.makeSound(); // 打印出"汪汪汪"。
}
}
通过抽象方法,我们可以定义一个规范或者接口,然后由具体的子类来实现这个规范或者接口的具体行为。这样可以提高代码的灵活性和可扩展性。
1.3 抽象类
1.3.1 抽象类的引入
既然我们现在已经了解了抽象方法,那么什么是抽象类呢?非常简单!抽象类就是可以存在抽象方法的类
所以说抽象类中可以有抽象方法,也可以没有抽象方法,但是一个类如果有抽象方法,那么这个类一定是抽象类,因为抽象方法必须存在于抽象类之中
因为抽象类不能具体描述一个对象,所以 java 规定了,抽象类不能够实例化对象,那么一个类如果不能实例化对象,这个类还有什么用处呢?答案是拿来继承
1.3.2 抽象类的语法
在 Java 中,一个类如果被 abstract 修饰称为抽象类
注意:抽象类也是类,和普通的方法一样,内部可以包含普通方法和属性,甚至构造方法,抽象类只是可以存在抽象方法而已
抽象类的特性:
- 抽象类不能直接实例化对象
- 抽象类用来被继承,并且继承后子类要重写父类中的抽象方法,否则子类也是抽象类,必须要使用 abstract 修饰
- 抽象类中不一定包含抽象方法,但是有抽象方法的类一定是抽象类
1.4 本人对抽象类的理解
我觉得抽象类中的抽象方法更像是一种行为规范,它规定了子类对这种行为进行改动,举个例子:
我们在父类 Animal 中定义了一个 bark()的抽象方法,这是不是就意味着,如果你是我的子类,那么你也应该会叫,但是怎么叫就取决与你这个对象是什么了
还有为什么抽象类中可以没有抽象方法?没有抽象方法为什么不定义成普通的类呢?抽象类不能被实例化这个特性很重要,当我们定义了一个类,并且不希望这个类实例化出对象,只希望这个类被继承,那么我们就可以将这个类定义成抽象类
总结:
- 抽象类就是拿来被继承的
- 抽象方法就是拿来被重写的
二:接口
2.1 接口的引入
下面我来举个例子来说明接口,假设我们有一个 u 盘和一台笔记本,u 盘中有很多功能,当我们需要使用 u 盘中的某个功能的时候,我们可以将 u 盘插入 USB 口,然后我们就可以使用 u 盘中的功能了
在 java 中,接口就是抽象方法的集合(可以理解成功能的集合),当我们实现了一个接口的时候,我们需要对接口中的抽象方法进行重写,当我们重写这些方法的时候,我们自然就有了这些功能
2.2 接口的语法
在 Java 中,接口是一种抽象类型,它定义了一组方法的规范,但没有提供方法的实现细节。其他类可以实现这个接口并提供具体的方法实现。
接口的声明使用 interface
关键字,并且可以包含方法签名、常量和默认方法。一个类可以实现一个或多个接口。
在 java 中,子类和父类之间是 extends 继承关系,类与接口之间是 implements 实现关系,接口和接口之间是 extends 拓展关系( extends 本意就是拓展 )。
接口的特性:
- 接口中有抽象方法,所以接口也不能实例化对象
- 接口中的方法默认为 public abstract,并且只能是这个(default除外)
- 接口中的变量默认为 public static final,并且只能是这个
- 接口中的抽象方法是不能有实现的,default 方法除外
- 如果类没有实现接口中所有的抽象方法,则类必须设置为抽象类
- 接口中不能有静态代码块和构造方法( 接口又不能实例化对象,要代码块和构造方法有什么用 )
下面是一个简单的示例,展示了如何声明和实现一个接口:
// 定义一个接口
interface Animal {
void makeSound(); // 抽象方法
default void eat() {
System.out.println("动物吃东西");
}
}
// 实现接口的类
class Dog implements Animal {
public void makeSound() {
System.out.println("狗在汪汪叫");
}
}
class Cat implements Animal {
public void makeSound() {
System.out.println("猫在喵喵叫");
}
}
public class Main {
public static void main(String[] args) {
Animal dog = new Dog();
Animal cat = new Cat();
dog.makeSound(); // 输出: 狗在汪汪叫
cat.makeSound(); // 输出: 猫在喵喵叫
dog.eat(); // 输出: 动物吃东西
cat.eat(); // 输出: 动物吃东西
}
}
2.3 本人对于接口的理解
2.3.1 对于接口中方法和变量的理解
为什么接口中的方法默认为 public abstract?并且只能是这个,为什么接口中的变量默认为 public static final?并且只能是这个
我的理解是这样的:
- 对于方法
因为接口中的方法需要被重写实现,所以我们通过 public,就能让接口对所有的类都可见,也就可以让所有的类去实现这个接口,重写这个接口内的抽象方法,从而具有接口中的功能了
而 abstract 则是为了不写方法体,并告诉别人有这个功能,有这个规范,当某个类去实现这个接口的时候,再进行重写就好了
- 对于变量
接口中的变量默认为 public static final,是因为接口中的变量被视为常量,其值在编译时确定并且不能被修改。这种变量被称为接口常量或者常量接口。
2.4 实现多个接口
在 Java 中,一个类可以实现多个接口,也可以继承一个类并实现一个接口。
首先,假设有两个接口:Flyable
和Swimable
,分别表示可飞行和可游泳的特性。我们定义一个Bird
类,它实现了这两个接口,并提供了相应的实现方法。
interface Flyable {
void fly();
}
interface Swimable {
void swim();
}
class Bird implements Flyable, Swimable {
@Override
public void fly() {
System.out.println("Bird is flying.");
}
@Override
public void swim() {
System.out.println("Bird is swimming.");
}
}
在上面的代码中,Bird
类同时实现了Flyable
和Swimable
接口。这意味着Bird
类具有飞行和游泳的能力。
接下来,我们可以创建一个Main
类来测试Bird
类的功能。
public class Main {
public static void main(String[] args) {
Bird bird = new Bird();
bird.fly(); // 输出:Bird is flying.
bird.swim(); // 输出:Bird is swimming.
}
}
通过上述代码,我们可以看到,Bird
类可以调用fly()
方法和swim()
方法,这是因为它实现了Flyable
和Swimable
接口。
接着是一个类继承了另一个类再实现了一个接口的例子(先继承再实现):
class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
}
interface IRunning {
void run();
}
class Cat extends Animal implements IRunning {
public Cat(String name) {
super(name);
}
@Override
public void run() {
System.out.println(this.name + "正在用四条腿跑");
}
}
接口之间的拓展:
interface IRunning {
void run();
}
interface ISwimming {
void swim();
}
// 两栖的动物, 既能跑, 也能游
interface IAmphibious extends IRunning, ISwimming {
}
class Frog implements IAmphibious {
...
}
接口间的继承相当于把多个接口合并在一起,通过接口继承创建一个新的接口 IAmphibious 表示 “两栖的”. 此时实现接口创建的 Frog 类, 就继续要实现 run 方法, 也需要实现 swim 方法.
三:String 类
3.1 引入字符串
在 C 语言中已经涉及到字符串了,但是在 C 语言中要表示字符串只能使用字符数组或者字符指针,我们可以使用标准库提供的字符串系列函数完成大部分操作。
但是这种将数据和操作数据方法分离开的方式不符合面相对象的思想,而字符串应用又非常广泛,因此Java语言专门提供了String类。
3.2 字符串的构造方法
String 类提供的构造方式非常多,常用的就以下三种:
public static void main(String[] args) {
// 使用常量串构造
String s1 = "hello bit";
System.out.println(s1);
// 直接newString对象
String s2 = new String("hello bit");
System.out.println(s2);
// 使用字符数组进行构造
char[] array = {'h','e','l','l','o','b','i','t'};
String s3 = new String(array);
System.out.println(s3);
}
注意:在 java 中,字符串没有所谓的以 \0 结尾,并且 String 是引用类型,它内部并不存储字符串本身
3.3 字符串在内存中的存储方式
在 Java 中,字符串被存储在特殊的内存区域中,称为字符串常量池(String Pool)。
当你创建一个字符串对象时,Java虚拟机(JVM)会首先检查字符串常量池中是否已经存在相同值的字符串。如果存在,那么新创建的字符串对象将直接引用已经存在的字符串常量池中的对象,而不会再创建新的对象。这个过程被称为"字符串驻留(String Interning)",它可以减少内存开销,提高性能。
如果字符串常量池中不存在相同的字符串,则会在字符串常量池中创建一个新的字符串对象,并将其引用返回给你。
字符串常量池的特点是字符串对象的值是不可变的。这意味着一旦创建了一个字符串对象,就不能再改变它的值。如果你对字符串进行了修改,实际上是创建了一个新的字符串对象。这种不可变性带来了一些优势,比如字符串可以被安全地用作 Map 的键、线程安全等。
需要注意的是,如果你使用 new 关键字创建字符串对象,例如:String str = new String(“Hello”);,则会在堆中创建一个新的字符串对象,而不是在字符串常量池中。
3.4 String 类的 value 和 hash
每个字符串在创建的时候都是不可变的,每个字符串都有着两个重要的属性
- value
- hash
value 用于存储字符数组的地址,通过 value 就能访问到字符串中的内容
而 hash 属性是一个整数,被用于字符串的哈希码,哈希码是通过将字符串转换成一个整数,用于快速判断字符串的相等性,Java 的 String 类中,hash 属性的值被缓存起来,以提高哈希码的计算效率。
下面通过一段代码和图解说明:
public static void main(String[] args) {
// s1和s2引用的是不同对象 s1和s3引用的是同一对象
String s1 = new String("hello");
String s2 = new String("world");
String s3 = s1;
}
注意:在 Java 中 “ ” 引起来的也是 String 类型对象
// 打印"hello"字符串(String对象)的长度
System.out.println("hello".length());
3.5 String 中的常用方法
3.5.1 字符串查找
方法 | 功能 |
---|---|
char charAt(int index) | 返回index位置上字符,如果index为负数或者越界,抛出IndexOutOfBoundsException异常 |
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 lastIndexOf(int ch) | 从后往前找,返回ch第一次出现的位置,没有返回-1 |
int lastIndexOf(int ch, int fromIndex) | 从fromIndex位置开始找,从后往前找ch第一次出现的位置,没有返回-1 |
int lastIndexOf(String str) | 从后往前找,返回str第一次出现的位置,没有返回-1 |
int lastIndexOf(String str, int fromIndex) | 从fromIndex位置开始找,从后往前找str第一次出现的位置,没有返回-1 |
下面是这些方法的使用示例代码:
public static void main(String[] args) {
String s = "aaabbbcccaaabbbccc";
System.out.println(s.charAt(3)); // 'b'
System.out.println(s.indexOf('c')); // 6
System.out.println(s.indexOf('c', 10)); // 15
System.out.println(s.indexOf("bbb")); // 3
System.out.println(s.indexOf("bbb", 10)); // 12
System.out.println(s.lastIndexOf('c')); // 17
System.out.println(s.lastIndexOf('c', 10)); // 8
System.out.println(s.lastIndexOf("bbb")); // 12
System.out.println(s.lastIndexOf("bbb", 10)); // 3
}
3.5.2 字符串转化
3.5.2.1 数字和字符串转化
在 Java中,我们可以使用两个方法实现字符串和数字之间的相互转换: parseInt()
和 valueOf()
。
- 使用
parseInt()
方法将字符串转换为整数:
String strNumber = "123";
int number = Integer.parseInt(strNumber);
System.out.println(number); // 输出: 123
- 使用
valueOf()
方法将整数转换为字符串:
int number = 123;
String strNumber = String.valueOf(number);
System.out.println(strNumber); // 输出: "123"
这些方法还支持其他数值类型的转换,例如Long
、Float
和Double
。比如 parseLong,parseFloat,parseDouble,而 valueof 重载了很多方法,会将括号中的内容直接变为字符串,并返回该字符串的地址
3.5.2.2 字符串大小写转换
在 Java 中,字符串的大小写转换可以使用两个方法:toUpperCase()
和 toLowerCase()
。下面是这两个方法的示例代码:
String originalString = "Hello World";
// 转换为大写
String uppercaseString = originalString.toUpperCase();
System.out.println(uppercaseString); // 输出: HELLO WORLD
// 转换为小写
String lowercaseString = originalString.toLowerCase();
System.out.println(lowercaseString); // 输出: hello world
3.5.2.3 字符串转字符数组
在 Java 中,字符串转换为数组可以使用 toCharArray()
方法。这个方法将字符串转换为 char 类型的数组。
下面是一个示例代码,:
public class StringToArrayExample {
public static void main(String[] args) {
String str = "Hello, World!";
// 将字符串转换为char数组
char[] charArray = str.toCharArray();
// 打印char数组的每个元素
for (char ch : charArray) {
System.out.println(ch);//这个函数会自动换行
}
}
}
运行上述代码,输出结果将是:
H
e
l
l
o
,
W
o
r
l
d
!
3.5.3 字符串替换
在Java中,字符串替换的方法包括 replaceAll()
和 replaceFirst()
。这两个方法都可以用于替换字符串中的指定字符序列。
replaceAll()
方法
replaceAll()
方法用于替换字符串中所有匹配的字符序列。它接受两个参数:第一个参数是要替换的字符序列的正则表达式,第二个参数是替换后的字符序列。
示例代码:
String str = "Hello World! Hello Java!";
String newStr = str.replaceAll("Hello", "Hi");
System.out.println(newStr);
输出结果:
Hi World! Hi Java!
replaceFirst()
方法
replaceFirst()
方法用于替换字符串中首次出现的匹配字符序列。它也接受两个参数:第一个参数是要替换的字符序列,第二个参数是替换后的字符序列。
示例代码:
String str = "Hello World! Hello Java!";
String newStr = str.replaceFirst("Hello", "Hi");
System.out.println(newStr);
输出结果:
Hi World! Hello Java!
3.5.4 字符串拆分
在 java 中,split()
方法常用于拆分字符串,它将字符串按照指定的分隔符拆分为多个子字符串,并返回一个字符串数组。
split()
方法有两个重载的形式,它们的区别在于传入的参数不同:
split(String regex)
:
这个方法接受一个正则表达式作为参数,用来指定分隔符。它会根据正则表达式匹配的位置将字符串拆分成多个子字符串,并返回一个字符串数组。
下面是一个使用split(String regex)
方法的示例代码:
String str = "Hello,World,Java";
String[] arr = str.split(",");
for(String s : arr) {
System.out.println(s);
}
运行结果:
Hello
World
Java
split(String regex, int limit)
:
这个方法除了接受正则表达式作为参数外,还接受一个额外的整数参数limit
,用于控制拆分后的子字符串的数量。
- 如果
limit
为正数,它将确定拆分的子字符串的最大数量; - 如果
limit
为负数,它将拆分字符串的所有部分,但尾部空字符串将被丢弃; - 如果
limit
为零,它将拆分字符串的所有部分,包括尾部空字符串。
下面是使用带有 limit
参数的 split(String regex, int limit)
方法的示例代码:
String str = "Hello,World,Java";
String[] arr = str.split(",", 2);
for(String s : arr) {
System.out.println(s);
}
运行结果:
Hello
World,Java
注意事项:
- 字符 " | ", " * " , " + " 都得加上转义字符,前面加上 “\” .
- 而如果是 " \ " ,那么就得写成 “\\” .
- 如果一个字符串中有多个分隔符,可以用 " | " 作为连字符.
3.5.5 字符串截取
Java 中的字符串类提供了两个重载的 substring() 方法,用于截取子字符串。下面我来解释一下这两个重载的方法
substring(int beginIndex)
:方法参数为开始索引,它返回从指定索引位置开始到原字符串末尾的子字符串。注意,索引从0开始计数。
示例代码:
String str = "Hello, World!";
String subStr = str.substring(7);
System.out.println(subStr); // 输出:World!
substring(int beginIndex, int endIndex)
:方法接受两个参数,开始索引(beginIndex)和结束索引(endIndex - 1),它返回从开始索引到结束索引之前的子字符串。即该方法会截取从开始索引到结束索引前一位的字符。
示例代码:
String str = "Hello, World!";
String subStr = str.substring(7, 12);
System.out.println(subStr); // 输出:World
请注意,这两个substring()方法均返回一个新的字符串,原字符串不会被修改
3.6 字符串的不可变性
在 Java 中,字符串是不可变的,这意味着一旦创建了一个字符串对象,它的值就不能被修改,换句话说,每次对字符串进行操作时,都会创建一个全新的字符串对象,而原始的字符串对象则保持不变。上述的所有字符串方法亦是如此。
所以如果我们要对字符串进行大量的修改,不应该使用 string 类的方法,而应该使用接下来说的 StringBuilder 和 StringBuffer 类中的方法。
虽然字符串的方法每次都会创建一个全新的字符串对象,但是字符串这种不可变性带来了一些好处:
-
线程安全性:由于字符串是不可变的,多个线程可以同时访问和共享字符串对象,而无需担心数据的修改冲突。
-
安全性:字符串在一些安全敏感的场景中非常有用。例如,在数据库连接字符串中,通过不可变性,可以确保连接字符串的值不会被恶意篡改。
-
HashCode 缓存:字符串的不可变性使得可以缓存它们的哈希码。因为哈希码是在字符串创建时计算的,一旦计算得出,它就不需要重新计算,提升了性能。
-
字符串池(String Pool):Java中的字符串池是一种内存优化技术,它利用字符串的不可变性来节省内存。当创建一个字符串时,首先在字符串池中查找是否已经存在相同值的字符串对象,如果存在,则返回池中的引用,如果不存在,则在字符串池中创建新的字符串对象。
因此,在Java中将字符串设置为不可变的是有一定的优势和用处的。
3.7 StringBuilder 和 StringBuffer
当我们在 Java 中处理字符串时,有时会遇到需要频繁地修改字符串的情况。而 Java 中的 String 类是不可变的,这意味着一旦创建了一个 String 对象,它的值就不能被改变。这就导致了每次对字符串进行修改操作时都会创建一个新的 String 对象,这对于大规模的字符串操作来说效率低下。
为了解决这个问题,Java 提供了两个可变的字符串类:StringBuilder 和 StringBuffer。这两个类提供了一系列方法来对字符串进行修改操作,并且不会每次都创建新的对象。
StringBuilder 和 StringBuffer 的作用几乎相同,唯一的区别在于 StringBuffer 是线程安全的,而 StringBuilder 是非线程安全的。线程安全意味着在多线程环境下可以安全地使用对象,而非线程安全则需要在多线程环境下进行适当的同步处理。由于 StringBuilder 不需要进行同步处理,所以在单线程环境下比 StringBuffer 更高效。
当我们需要对字符串进行频繁的修改操作时,可以使用 StringBuilder 或 StringBuffer 来替代String。这样可以避免创建大量的临时字符串对象,提高程序的性能。
下面是一个简单的示例代码,演示了 StringBuilder 和 StringBuffer 的使用:
StringBuilder sb1 = new StringBuilder();
sb1.append("Hello");
sb1.append(" ");
sb1.append("World");
String result1 = sb1.toString(); // "Hello World"
StringBuffer sb2 = new StringBuffer();
sb2.append("Hello");
sb2.append(" ");
sb2.append("World");
String result2 = sb2.toString(); // "Hello World"
在上面的例子中,我们首先创建了一个空的 StringBuilder 或 StringBuffer 对象,然后使用 append()
方法逐步向其中添加字符串。最后,我们使用 toString()
方法将可变的字符串转换为不可变的 String 对象。