抽象类
定义与作用
抽象方法是使用abstract修饰且不包含具体实现的方法。其语法形式为:
public abstract 返回类型 方法名(参数列表);抽象方法没有方法体(即没有{}内容),仅声明方法签名。它主要用于提供一个通用的模板,定义一组共有的属性和行为,供子类继承并实现具体逻辑。
抽象类与抽象方法的关系
- 一个类如果包含至少一个抽象方法,那么该类必须声明为抽象类。
- 抽象类可以包含零个或多个抽象方法。。
abstract class Animal { protected String name; public Animal(String name) { this.name = name; } // 抽象方法:子类必须实现 public abstract void makeSound(); // 具体方法:可直接使用 public void sleep() { System.out.println(name + " is sleeping."); } } class Dog extends Animal { public Dog(String name) { super(name); } @Override public void makeSound() { System.out.println("Woof! Woof!"); } } class Cat extends Animal { public Cat(String name) { super(name); } @Override public void makeSound() { System.out.println("Meow! Meow!"); } }
- 抽象类不能被实例化,只能通过子类创建对象。
- 子类继承抽象类时,必须实现所有抽象方法,除非子类也声明为抽象类。
- 抽象类可以有构造函数,用于初始化公共字段。
- 抽象类支持多态,可通过父类引用指向子类对象。
接口(Interface)
是Java中一种特殊的引用类型,用于定义一组方法的签名,但不包含具体实现。它规定了实现类必须提供哪些方法。接口中的所有方法默认为public abstract,且不能包含实例变量(只能有常量)。
public interface 接口名称 extends 父类接口1, 父类接口2 { // 常量定义(默认为 public static final) 数据类型 常量名 = 初始值; // 抽象方法声明(默认为 public abstract) 返回类型 方法名(参数列表); // 默认方法(default method) default 返回类型 默认方法名(参数列表) { // 方法体 } // 静态方法(static method) static 返回类型 静态方法名(参数列表) { // 方法体 } // 私有方法(private method) private void 私有方法名() { // 方法体 } }实现接口的类
一个类可以通过implements关键字实现一个或多个接口。一个类实现接口时,必须实现所有抽象方法,否则该类也必须声明为抽象类。
public interface Animal { void makeSound(); void move(); }public class Dog implements Animal { @Override public void makeSound() { System.out.println("Woof!"); } @Override public void move() { System.out.println("Running on four legs."); } }实现接口的类可以被当作接口类型引用,实现多态:
Animal animal = new Dog(); animal.makeSound(); // 输出: Woof! animal.move(); // 输出: Running on four legs.多个接口的实现
一个类可以实现多个接口,使用逗号分隔:
public interface Flyable { void fly(); } public interface Swimmable { void swim(); } public class Duck implements Flyable, Swimmable { @Override public void fly() { System.out.println("Duck is flying."); } @Override public void swim() { System.out.println("Duck is swimming."); } }内部类
基本概念
在Java中,内部类是指定义在另一个类内部的类。内部类可以访问外部类的所有成员,包括私有成员。根据定义位置和使用方式的不同,内部类分为四种类型:成员内部类、局部内部类、匿名内部类和静态内部类。
成员内部类
成员内部类是定义在外部类中的普通类,与外部类的成员变量和方法处于同一级别。它可以直接访问外部类的任何成员,包括私有成员。
public class Work { public static void main(String[] args) { // 创建内部类对象的两种方式 OutClass out = new OutClass(); OutClass.InClass inner = out.new InClass(); inner.show(); OutClass.InClass a = new OutClass().new InClass(); a.show(); } } class OutClass { private int age = 18; public class InClass { int age = 20; public void show() { int age = 30; System.out.println("外部类变量:" + OutClass.this.age); // 外部类变量:18 System.out.println("内部类变量:" + this.age); // 内部类变量:20 System.out.println("局部变量:" + age); // 局部变量:30 } } }静态内部类
静态内部类是使用static修饰的内部类,它不依赖于外部类的实例。因此,可以在不创建外部类对象的情况下直接创建静态内部类的实例。
public class OuterClass { static class StaticInnerClass { // 静态内部类的内容 } }OuterClass.StaticInnerClass inner = new OuterClass.StaticInnerClass();静态内部类不能直接访问外部类的非静态成员,只能访问静态成员。
局部内部类
局部内部类是在方法或代码块中定义的内部类。它的作用域仅限于该方法或代码块内。
public class OuterClass { public void method() { final int localVar = 30; class LocalInnerClass { public void display() { System.out.println("Local variable: " + localVar); } } LocalInnerClass inner = new LocalInnerClass(); inner.display(); } }局部内部类只能访问外部方法中被声明为final或等效不可变的局部变量。
匿名内部类
匿名内部类是没有名字的内部类,通常用于简化代码,特别是在需要实现接口或继承类但只使用一次的情况下。
new 父类名或接口名() { // 类体内容:方法重写、变量定义等 };public interface Greeting { void sayHello(); } public class Main { public static void main(String[] args) { Greeting greeting = new Greeting() { @Override public void sayHello() { System.out.println("Hello from anonymous class!"); } }; greeting.sayHello(); } }匿名内部类常用于事件监听器、线程创建等场景。
String类
String 类是 Java 中用于表示字符串的不可变类,定义在 java.lang 包中。它封装了一个字符数组,用来存储字符串内容。由于其不可变性,一旦创建,字符串的内容无法更改。
字符串的创建方式
通过字面量创建:
String str1 = "Hello";通过构造方法创建:
String str2 = new String("World"); String str3 = new String(new char[]{'a', 'b', 'c'});常用方法
获取字符串长度:
int length = str.length();获取指定位置字符:
char ch = str.charAt(0);比较字符串内容是否相等(推荐使用 equals):
boolean isEqual = str1.equals(str2);忽略大小写比较:
boolean isEqualIgnoreCase = str1.equalsIgnoreCase(str2);查找子字符串位置:
int index = str.indexOf("world");截取子字符串:
String sub = str.substring(0, 5);替换字符或子字符串:
String replaced = str.replace('a', 'b'); String replacedAll = str.replaceAll("old", "new");分割字符串:
String[] parts = str.split(",");转换为大写或小写:
String upper = str.toUpperCase(); String lower = str.toLowerCase();去除首尾空白字符:
String trimmed = str.trim();判断是否以某前缀或后缀开始/结束:
boolean startsWith = str.startsWith("Hello"); boolean endsWith = str.endsWith("World");String 的不可变性
String 对象一旦创建,其内容不能被修改。任何看似“修改”的操作实际上都会生成新的字符串对象。例如:
String s = "abc"; s = s + "def"; // 产生新对象,原对象不变这种设计提高了安全性,尤其在多线程环境下,避免了共享字符串被意外修改的问题。
字符串常量池(String Pool)
Java 在堆内存中维护一个特殊的区域——字符串常量池,用于存放字面量形式的字符串。当使用字面量创建字符串时,如果池中已存在相同内容的字符串,则返回池中的引用,避免重复创建。
String a = "hello"; String b = "hello"; System.out.println(a == b); // true,指向同一对象使用 new 构造时,不会检查常量池,总是创建新对象:
String c = new String("hello"); String d = new String("hello"); System.out.println(c == d); // false,不同对象字符串拼接优化
频繁拼接字符串应使用 StringBuilder 或 StringBuffer,避免因大量临时对象导致性能下降。
StringBuilder sb = new StringBuilder(); sb.append("a").append("b").append("c"); String result = sb.toString();比较字符串的正确方式
使用==比较的是引用地址,仅当两个变量指向同一个对象时才为真。若需比较内容,必须使用.equals()。
String s1 = "test"; String s2 = new String("test"); System.out.println(s1 == s2); // false System.out.println(s1.equals(s2)); // true数组深入
数组拷贝
System.arraycopy() 方法
System.arraycopy(src, srcPos, dest, destPos, length);
src:源数组。srcPos:源数组的起始位置。dest:目标数组。destPos:目标数组的起始位置。length:要复制的元素数量。
int[] original = {1, 2, 3, 4, 5}; int[] copy = new int[original.length]; System.arraycopy(original, 0, copy, 0, original.length);Arrays.copyOf() 方法
Arrays.copyOf(original, newLength);
- 创建一个指定长度的新数组,并将原数组的内容复制到新数组中。
- 如果新长度大于原数组,多余位置用默认值填充(如 0)。
int[] original = {1, 2, 3, 4, 5}; int[] copy = Arrays.copyOf(original, original.length);Arrays.copyOfRange() 方法进行部分拷贝
Arrays.copyOfRange(original, from, to);
- 从原数组的
from索引开始,到to索引结束(不包含to)。- 返回一个新数组。
int[] original = {1, 2, 3, 4, 5}; int[] partialCopy = Arrays.copyOfRange(original, 1, 4); // [2, 3, 4]clone() 方法进行浅拷贝
int[] copy = original.clone();
- 对于基本类型数组,
clone()方法返回一个新数组,内容与原数组相同。- 适用于一维基本类型数组。
int[] original = {1, 2, 3, 4, 5}; int[] copy = original.clone();多维数组的拷贝
对于多维数组,clone()只进行浅拷贝,即只复制外层数组,内层数组仍指向原对象。
int[][] original = {{1, 2}, {3, 4}}; int[][] copy = original.clone(); // 此时,copy[0] 和 original[0] 指向同一数组,修改会影响彼此若需深拷贝,需手动遍历并复制每一维:
int[][] deepCopy = new int[original.length][]; for (int i = 0; i < original.length; i++) { deepCopy[i] = original[i].clone(); }
System.arraycopy():性能最高,适合大量数据复制。Arrays.copyOf()/copyOfRange():代码简洁,适合一般场景。clone():最简单,但仅限于一维基本类型数组。- 多维数组必须手动实现深拷贝以避免引用共享问题。
数组删除
在Java中,数组是固定长度的数据结构,一旦创建便无法直接修改其大小。因此,无法直接从数组中删除元素。但可以通过以下几种方式实现“删除”效果:
使用集合类
将数组转换为List,利用集合类提供的删除方法,再转回数组。
import java.util.*; // 原始数组 Integer[] arr = {1, 2, 3, 4, 5}; // 转换为列表 List<Integer> list = new ArrayList<>(Arrays.asList(arr)); // 删除指定位置的元素(例如索引为2) list.remove(2); // 转回数组 arr = list.toArray(new Integer[0]); System.out.println(Arrays.toString(arr)); // [1, 2, 4, 5]此方法适用于需要频繁增删操作的场景,灵活性高。
手动创建新数组(适用于删除单个元素)
通过遍历原数组,跳过要删除的元素,创建一个新数组。
int[] arr = {1, 2, 3, 4, 5}; int indexToDelete = 2; // 删除索引为2的元素(值为3) int[] newArr = new int[arr.length - 1]; for (int i = 0, j = 0; i < arr.length; i++) { if (i != indexToDelete) { newArr[j++] = arr[i]; } } arr = newArr; System.out.println(Arrays.toString(arr)); // [1, 2, 4, 5]适用于已知删除位置的情况,效率较高。
删除指定值的元素(多个匹配)
若需删除所有等于某个值的元素,可使用类似方法二的方式。
int[] arr = {1, 2, 3, 2, 4, 2}; int valueToDelete = 2; List<Integer> list = new ArrayList<>(); for (int num : arr) { if (num != valueToDelete) { list.add(num); } } arr = list.stream().mapToInt(Integer::intValue).toArray(); System.out.println(Arrays.toString(arr)); // [1, 3, 4]适合删除特定值的所有实例。
使用Arrays.copyOfRange(简化版)
当知道要删除的索引时,可直接使用Arrays.copyOfRange拷贝部分数组。
int[] arr = {1, 2, 3, 4, 5}; int indexToDelete = 2; int[] newArr = Arrays.copyOfRange(arr, 0, indexToDelete); int[] tail = Arrays.copyOfRange(arr, indexToDelete + 1, arr.length); // 合并两个部分 int[] result = new int[newArr.length + tail.length]; System.arraycopy(newArr, 0, result, 0, newArr.length); System.arraycopy(tail, 0, result, newArr.length, tail.length); arr = result; System.out.println(Arrays.toString(arr)); // [1, 2, 4, 5]适合一次性删除操作。
数组扩容
在 Java 中,原生数组的大小是固定的,一旦创建便无法改变。因此,当需要动态增加元素数量时,必须通过手动实现扩容机制。常见的做法是创建一个更大的新数组,并将原数组中的元素复制到新数组中。
System.arraycopy() 方法
创建一个新的更大容量的数组,使用System.arraycopy()方法高效复制原数组内容。
public class DynamicArray { private int[] data; private int size; public DynamicArray() { data = new int[10]; // 初始容量 size = 0; } public void add(int element) { if (size == data.length) { // 扩容:创建新数组,长度为原数组的 1.5 倍 int newCapacity = data.length + data.length / 2; int[] newData = new int[newCapacity]; System.arraycopy(data, 0, newData, 0, data.length); data = newData; } data[size++] = element; } }ArrayList()方法
内部已实现自动扩容逻辑
import java.util.ArrayList; ArrayList<Integer> list = new ArrayList<>(); list.add(1); list.add(2); // 底层自动处理扩容,无需手动干预Arrays工具类
数组排序方法
使用Arrays.sort()对数组进行排序,支持基本类型和对象类型。
int[] numbers = {64, 34, 25, 12, 22, 11, 90}; Arrays.sort(numbers);支持指定范围排序:
Arrays.sort(numbers, 1, 5); // 对索引1到4的元素排序数组查找方法
使用Arrays.binarySearch()在已排序数组中查找元素。
int[] arr = {10, 20, 30, 40, 50}; int index = Arrays.binarySearch(arr, 30); // 返回2若未找到,返回负值,表示插入位置。
数组比较方法
使用Arrays.equals()比较两个数组是否相等。
int[] a = {1, 2, 3}; int[] b = {1, 2, 3}; boolean isEqual = Arrays.equals(a, b); // true对于多维数组,使用Arrays.deepEquals():
int[][] matrix1 = {{1, 2}, {3, 4}}; int[][] matrix2 = {{1, 2}, {3, 4}}; boolean deepEqual = Arrays.deepEquals(matrix1, matrix2); // true数组填充方法
使用Arrays.fill()将数组元素设置为指定值。
int[] arr = new int[5]; Arrays.fill(arr, 7); // 所有元素变为7支持部分填充:
Arrays.fill(arr, 1, 4, 9); // 索引1到3设为9数组转字符串
使用Arrays.toString()将数组转换为可读字符串。
int[] arr = {1, 2, 3}; String str = Arrays.toString(arr); // "[1, 2, 3]"多维数组
二维数组的初始化
声明并使用 new 指定大小
// 声明一个 3 行 4 列的二维数组 int[][] matrix = new int[3][4]; // 可以逐个赋值 matrix[0][0] = 1; matrix[0][1] = 2; matrix[1][2] = 3; // ...直接初始化(静态初始化)
// 初始化一个 2×3 的二维数组 int[][] matrix = { {1, 2, 3}, {4, 5, 6} };每一行必须用
{}包裹,且每行元素数量可以不同(即不规则数组)
混合初始化
int[][] matrix = new int[3][]; matrix[0] = new int[]{1, 2}; matrix[1] = new int[]{3, 4, 5}; matrix[2] = new int[]{6};使用二维数组保存表格数据
直接初始化创建
// 声明并初始化一个3行4列的二维数组 String[][] table = { {"姓名", "年龄", "城市", "职业"}, {"张三", "25", "北京", "工程师"}, {"李四", "30", "上海", "设计师"} }; // 遍历并输出表格内容 for (int i = 0; i < table.length; i++) { for (int j = 0; j < table[i].length; j++) { System.out.print(table[i][j] + "\t"); } System.out.println(); }动态创建二维数组
// 创建一个指定行数和列数的二维数组 int rows = 3; int cols = 4; String[][] dynamicTable = new String[rows][cols]; // 为数组赋值 dynamicTable[0][0] = "姓名"; dynamicTable[0][1] = "年龄"; dynamicTable[0][2] = "城市"; dynamicTable[0][3] = "职业"; dynamicTable[1][0] = "王五"; dynamicTable[1][1] = "28"; dynamicTable[1][2] = "广州"; dynamicTable[1][3] = "教师";处理不规则表格(非矩形)
// 不同行长度不同的情况(非规则二维数组) String[][] irregularTable = { {"产品", "价格"}, {"手机", "5000", "品牌"}, {"电脑", "8000", "配置", "重量"} };// 使用固定宽度对齐输出 public static void printTable(String[][] table) { for (String[] row : table) { for (String cell : row) { System.out.printf("%-10s", cell); // 左对齐,占10个字符宽度 } System.out.println(); } } // 调用方法 printTable(table);冒泡排序
冒泡排序是一种简单的排序算法,其核心思想是通过重复遍历数组,比较相邻元素并根据大小关系进行交换,使得较大的元素逐渐“冒泡”到数组的末尾。每一轮遍历都会将当前未排序部分的最大值移动到正确位置。
实现方式一:基础冒泡排序(升序)
public class BubbleSort { public static void bubbleSort(int[] arr) { int n = arr.length; for (int i = 0; i < n - 1; i++) { for (int j = 0; j < n - i - 1; j++) { if (arr[j] > arr[j + 1]) { // 交换元素 int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } } } public static void main(String[] args) { int[] numbers = {64, 34, 25, 12, 22, 11, 90}; System.out.println("排序前:" + java.util.Arrays.toString(numbers)); bubbleSort(numbers); System.out.println("排序后:" + java.util.Arrays.toString(numbers)); } }实现方式二:优化版冒泡排序(提前终止)
当某一轮遍历中没有发生任何交换时,说明数组已经有序,可以提前结束排序过程。
public class OptimizedBubbleSort { public static void bubbleSort(int[] arr) { int n = arr.length; for (int i = 0; i < n - 1; i++) { boolean swapped = false; for (int j = 0; j < n - i - 1; j++) { if (arr[j] > arr[j + 1]) { int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; swapped = true; } } // 如果本轮没有交换,说明已排序完成 if (!swapped) break; } } public static void main(String[] args) { int[] numbers = {5, 2, 8, 1, 9}; System.out.println("排序前:" + java.util.Arrays.toString(numbers)); bubbleSort(numbers); System.out.println("排序后:" + java.util.Arrays.toString(numbers)); } }降序冒泡排序
只需将比较条件从>改为<即可实现降序排列。
if (arr[j] < arr[j + 1]) { int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; }
- 外层循环控制排序轮数,最多需要
n-1轮。- 内层循环每次减少一个比较次数,因为每轮后最大值已归位。
- 避免越界访问,注意
j < n - i - 1的边界条件。
二分查找
二分查找是一种在有序数组中查找特定元素的高效算法。其核心思想是每次将搜索范围缩小一半,通过比较目标值与中间元素的大小关系,决定下一步在左半部分还是右半部分继续查找。
适用条件
- 数组必须是有序的(升序或降序)。
- 支持随机访问(如数组),不适用于链表等结构。
实现方式一:递归实现
public static int binarySearchRecursive(int[] arr, int target, int left, int right) { if (left > right) { return -1; // 未找到 } int mid = left + (right - left) / 2; if (arr[mid] == target) { return mid; } else if (arr[mid] > target) { return binarySearchRecursive(arr, target, left, mid - 1); } else { return binarySearchRecursive(arr, target, mid + 1, right); } }int[] arr = {1, 3, 5, 7, 9, 11}; int index = binarySearchRecursive(arr, 7, 0, arr.length - 1); System.out.println(index); // 输出: 3实现方式二:迭代实现(推荐)
public static int binarySearchIterative(int[] arr, int target) { int left = 0; int right = arr.length - 1; while (left <= right) { int mid = left + (right - left) / 2; if (arr[mid] == target) { return mid; } else if (arr[mid] > target) { right = mid - 1; } else { left = mid + 1; } } return -1; // 未找到 }int[] arr = {1, 3, 5, 7, 9, 11}; int index = binarySearchIterative(arr, 7); System.out.println(index); // 输出: 3
- 使用
left + (right - left) / 2而非(left + right) / 2可防止整数溢出。- 若数组中有重复元素,返回的索引是不确定的,取决于具体实现和数据分布。
- 若需查找第一个或最后一个重复元素,需对标准二分查找进行扩展。
查找插入位置(左边界)
public static int lowerBound(int[] arr, int target) { int left = 0; int right = arr.length; while (left < right) { int mid = left + (right - left) / 2; if (arr[mid] < target) { left = mid + 1; } else { right = mid; } } return left; }该方法返回第一个大于等于target的元素位置,可用于插入排序或去重场景。