面向对象
设计对象并使用
- 类(设计图):是对象共同特征的描述;
- 对象:是真实存在的具体东西
- 在Java中,必须先设计类,才能获得对象。
如何获得对象
arduino
public class 类名 {
1.成员变量(代表属性的,一般是名词)
2.成员变方法(代表行为的,一般是动词)
}
类名 变量名=new 类名();
定义类的补充注意事项
- 用来描述一类事物的类,专业叫做JavaBean类。在JavaBean类中,是不写main方法的。
- 在以前,编写的main方法的类,叫做测试类。我们可以在测试类中创建JavaBean类的对象并进行赋值调用
- 类名首字母建议大写,需要见名知意,驼峰模式
- 一个java文件中可以定义多个class类,且只能一个类是public修饰,而且public修饰的类名必须成为代码文件名
- 成员变量的完整定义格式是:修饰符 数据类型 变量名称=初始化值;一般无需指定初始化值,存在默认值。
封装
告诉我们,如何正确的设计对象的属性和方法 原则:对象代表什么,就得封装对应的数据,并提供数据对应的行为
好处:
- 让编程变得简单,有什么事,找对象,调方法就行
- 降低我们的学习成本,可以少学,少记,或者说压根不用学,不用记对象有哪些方法,有需要时去找就行
this关键字
概念
this是java中的一个重要关键字,表示当前对象的引用,可以理解为这个对象,它指向当前正在调用方法或构造器的对象实例
主要用途
- 解决成员变量与局部变量同名问题
- 在构造器中调用其他构造器(构造器重载)
- 这种调用必须放在构造器的第一行
- 一个构造器内只能调用一次其他构造器
- 作为参数传递
- 返回当前对象
使用限制
- this只能在非静态方法中使用(静态方法没有this概念),因为静态方法属于类而非对象,调用时可能没有对象实例的存在
- 不能在静态代码块中使用
- 构造器调用(this())必须放在构造器第一行
实际应用
示例一
Builder模式
arduino
public class User {
private final String name;
private final int age;
private User(Builder builder) {
this.name = builder.name;
this.age = builder.age;
}
public static class Builder {
private String name;
private int age;
public Builder name(String name) {
this.name = name;
return this;
}
public Builder age(int age) {
this.age = age;
return this;
}
public User build() {
return new User(this);
}
}
}
// 使用
User user = new User.Builder()
.name("张三")
.age(25)
.build();
示例二
链式调用
csharp
public class Calculator {
private int result;
public Calculator add(int value) {
this.result += value;
return this;
}
public Calculator subtract(int value) {
this.result -= value;
return this;
}
public int getResult() {
return this.result;
}
}
// 使用
int result = new Calculator()
.add(10)
.subtract(5)
.add(20)
.getResult();
构造方法(Constructor)
构造方法是一种特殊的方法,用于创建对象时初始化对象。
基本概念
- 作用:初始化对象
- 特点:
- 没有返回类型(连void都没有)
- 方法名必须跟类名完全相同
- 可以重载(一个类可以有多个不同参数的构造方法)
基本语法
arduino
public class MyClass {
// 无参构造方法
public MyClass() {
// 初始化代码
}
// 带参数的构造方法
public MyClass(int param1, String param2) {
// 初始化代码
}
}
构造方法类型
- 默认构造方法
- 如果类中没有定义任何构造方法,编译器会自动提供一个无参的默认构造方法
- 如果定义了至少一个构造方法,编译器不会提供默认的构造方法
- 无参构造方法
- 带参构造方法
super关键字
super是Java中的一个重要关键字,主要用于在子类中访问父类的成员(属性,方法和构造方法)
主要用途
- 调用父类的构造方法
在子类构造方法中,必须首先调用父类的构造方法(显式或者隐式)。如果不显式调用,编译器会自动插入super()(调用父类无参构造方法)。
scala
class Parent {
Parent() {
System.out.println("父类无参构造方法");
}
Parent(String name) {
System.out.println("父类带参构造方法: " + name);
}
}
class Child extends Parent {
Child() {
// 隐式调用 super();
System.out.println("子类无参构造方法");
}
Child(String name) {
super(name); // 显式调用父类带参构造方法
System.out.println("子类带参构造方法");
}
}
重要规则:
- super()必须是子类构造方法的第一条语句
- 如果父类没有无参构造方法,子类必须显式调用父类的其他构造方法
- 访问父类的成员变量
当子类和父类有同名成员变量时,可以用super区分
scala
class Parent {
String name = "父类名称";
}
class Child extends Parent {
String name = "子类名称";
void printNames() {
System.out.println(name); // 输出子类名称
System.out.println(super.name); // 输出父类名称
}
}
- 调用父类方法
当子类重写了父类方法,但仍想调用父类的原始实现时
scala
class Parent {
void show() {
System.out.println("父类show方法");
}
}
class Child extends Parent {
@Override
void show() {
super.show(); // 先调用父类方法
System.out.println("子类show方法");
}
}
使用注意事项
- 必须在子类中使用:super只能在子类中使用,普通类中使用会报错
- 构造方法调用规则:
- 每个构造方法最多只能有一个super()调用
- 如果没有显示调用,编译器会自动插入super()
- super()和this()不能同时出现在一个构造方法中
- 静态上下文中不可用:不能在静态方法,静态代码块中使用super
实际应用实例
typescript
// 父类
class Animal {
String name;
Animal(String name) {
this.name = name;
}
void eat() {
System.out.println(name + "正在吃东西");
}
}
// 子类
class Dog extends Animal {
String breed;
Dog(String name, String breed) {
super(name); // 必须调用父类构造方法
this.breed = breed;
}
@Override
void eat() {
super.eat(); // 先执行父类eat方法
System.out.println(name + "正在啃骨头");
}
void display() {
System.out.println("名字: " + super.name + ", 品种: " + breed);
}
}
/**
* @author ZG
*/ // 使用
public class Main {
public static void main(String[] args) {
Dog myDog = new Dog("旺财", "金毛");
myDog.eat();
myDog.display();
}
}
输出结果:
旺财正在吃东西
旺财正在啃骨头
名字: 旺财, 品种: 金毛
常见面试题
- super和this的区别
- this引用当前对象,super引用父类成员
- this()调用本类其他构造方法,super()调用父类构造方法
- this可以在任何非静态方法中使用,super只能在子类中使用
- 为什么子类构造方法必须调用父类构造方法?
- 确保父类的初始化先于子类
- 保证对象创建的完整性(先有父才有子)
- 能否在静态方法中使用super?
- 不能,因为super与对象实例相关,而静态方法不依赖于实例
标准JavaBean
JavaBean是符合特定规范的java类,主要用于封装数据。以下是标准的JavaBean规范
基本要求:
- 公共无参构造方法:必须提供一个public修饰的无参构造方法
- 属性私有化:所有属性必须声明为private
- Getter/Setter方法:每个属性提供pubilc的getter和setter方法
- 可序列化:实现
Serializable
接口(可选但推荐)
标准示例
arduino
public class User implements Serializable {
// 属性私有化
private Long id;
private String username;
private String password;
private Integer age;
// 公共无参构造方法
public User() {
}
// 带参构造方法(可选)
public User(Long id, String username, String password, Integer age) {
this.id = id;
this.username = username;
this.password = password;
this.age = age;
}
// Getter 和 Setter 方法
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
// 可选:重写 toString() 方法
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + ''' +
", password='" + password + ''' +
", age=" + age +
'}';
}
// 可选:重写 equals() 和 hashCode() 方法
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(id, user.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
Getter/Setter方法命名规范
- Getter方法:
- 命名格式:get+属性名(首字母大写)
- 布尔类型可以使用is+属性名(首字母大写)
- Setter方法:
- 命名格式:set+属性名(首字母大写)
- 返回类型为void,带一个属性类型相同的参数
为什么使用JavaBean
- 封装性:隐藏实现细节,只暴露必要的访问方法
- 可重用性:标准化的类结果便于重用
- 工具支持:许多框架(如Spring、Hibernate)依赖JavaBean规范
- 可序列化:便于网络传输和持久化存储
JavaBean在框架中的使用
- Spring MVC:自动将请求参数绑定到 JavaBean
- Hibernate/JPA:将数据库记录映射为 JavaBean
- Jackson/Gson:将 JSON/XML 与 JavaBean 相互转换
对象内存图
基本内存结构
Java内存主要分为以下几个区域:
- 栈(Stack):存储局部变量和方法调用
- 堆(Heap):存储对象实例和数组
- 方法区(Method Area):存储类信息,常量,静态变量等
- 程序计数器:线程私有,记录当前线程执行的位置
- 本地方法栈:为本地方法服务
对象内存图示例
ini
class Person {
String name;
int age;
void sayHello() {
System.out.println("Hello, I'm " + name);
}
}
public class Main {
public static void main(String[] args) {
Person p1 = new Person();
p1.name = "张三";
p1.age = 25;
Person p2 = p1;
p2.age = 30;
p1.sayHello();
}
}
详细解释
:
- 栈内存:
- main方法的栈帧中包含局部变量p1和p2
- p1存储的是Person对象在堆中的地址值(引用)
- p2=p1使得p2也指向同一个对象
- 堆内存:
- 存储实际的Person对象实例
- 包含两个字段:name(引用类型)和age(基本类型)
- name字段指向字符串常量池中的"张三"
- 方法区:
- 存储Person类的元数据
- 包含方法信息(如sayHello)
- 包含字段的类型信息
重要结论
- 对象引用是地址:变量存储的是对象的引用(地址),不是对象本身
- 多引用指向同一对象:多个引用可以指定向同一个对象
- 基本类型直接存储值:基本类型变量直接存储值,不是引用
- 方法调用不影响原始引用:方法参数传递是值传递(对于引用类型是传递引用的值)
成员和局部
字符串(String)类
String
String是Java中最常用的类之一,用于表示和操作字符串。以下是关于String类的全面介绍:
String的基本特性
- 不可变性:String对象一旦创建就不能被修改
- 存储在字符串常量池:提高字符串的重用效率
- final类:不能被继承
- 实现了多个接口:
Serializable
、Comparable<String>
、CharSequence
创建String对象的两种方式
ini
//方式一:使用字面量(推荐)
String s1 = "hello"; // 存储在字符串常量池
String s2 = "hello"; // 重用常量池中的对象
System.out.println(s1 == s2); // true,相同引用
javascript
//方式二:使用new关键字
String s3 = new String("hello"); // 在堆中创建新对象
String s4 = new String("hello"); // 另一个新对象
System.out.println(s3 == s4); // false,不同引用
字符串常量池(String Pool)
- 位置:Java之前位于方法区,Java之后位于堆内存
- 作用:避免重复创建相同的字符串对象
- intern()方法:将字符串显式放入常量池
ini
String s5 = new String("world").intern();
String s6 = "world";
System.out.println(s5 == s6); // true
String常用方法
ini
//获取信息
String str = "Hello World";
int length = str.length(); // 11
char ch = str.charAt(1); // 'e'
int index = str.indexOf('o'); // 4
//字符串比较
String a = "hello";
String b = "HELLO";
boolean eq1 = a.equals(b); // false
boolean eq2 = a.equalsIgnoreCase(b); // true
int cmp = a.compareTo(b); // 32 (h > H)
//字符串操作
String sub = str.substring(6); // "World"
String upper = str.toUpperCase(); // "HELLO WORLD"
String lower = str.toLowerCase(); // "hello world"
String trimmed = " hello ".trim(); // "hello"
//字符串检查
boolean starts = str.startsWith("Hello"); // true
boolean ends = str.endsWith("ld"); // true
boolean contains = str.contains("or"); // true
boolean empty = "".isEmpty(); // true
//字符串转换
char[] chars = str.toCharArray();
String joined = String.join("-", "a", "b", "c"); // "a-b-c"
String formatted = String.format("Name: %s, Age: %d", "Tom", 25);// Name: Tom, Age: 25
字符串拼接
ini
// +操作符
String s1 = "a" + "b" + "c"; // 编译时优化为 "abc"
String s2 = s1 + 123; // 运行时创建新对象
String s3 = "abc";
String s4 = new String("abc");
System.out.println(s1 == s3);// true
System.out.println(s1 == s4);// false
System.out.println(s2);// "abc123"
// concat() 方法
String s5="abcd";
String s6="abcdefg";
String s7 = s5.concat("efg");
System.out.println(s6==s7);// false
String的不可变性
- 不可变性的实现原理:
- Stirng类被声明为final
- 内部字符数组
private final char value[]
(java9之后改为了btye[]) - 没有提供修改内部数组的方法
- 不可变性的好处:
- 线程安全
- 可以缓存哈希值(提高HashMap等性能)
- 安全性(如网络连接参数,文件路径等)
- 适合作为HashMap的键
性能优化建议
- 循环中使用StringBuilder:
ini
// 错误做法 - 每次循环创建新String对象
String result = "";
for (int i = 0; i < 100; i++) {
result += i;
}
// 正确做法
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100; i++) {
sb.append(i);
}
result = sb.toString();
- 使用字符串字面量而非new String():
ini
// 推荐
String s1 = "hello";
// 不推荐 - 除非有特殊需要
String s2 = new String("hello");
- 合理使用intern()方法
- 适合大量重复字符场景
- 不适合少量或不重复字符串(会增加常量池的负担)
Java9+的String优化
从Java9开始,String内部改用byte[]存储:
- 拉丁字符使用1个字节存储
- 非拉丁字符使用2字节存储
- 新增coder字段标识编码方式
- 节省内存空间(特别是大量英文文本)
Stirng vs StringBuilder vs StringBuffer
特性 | String | StringBuilder | StringBuffer |
---|---|---|---|
可变性 | 不可变 | 可变 | 可变 |
线程安全性 | 是(天然) | 否 | 是(同步方法) |
性能 | 低(修改操作) | 高 | 中 |
使用场景 | 常量字符串 | 单线程字符串操作 | 多线程字符串操作 |
StringJoiner
StringJoiner
是Java8引入的一个实用类,用于高效拼接字符串,支持分隔符、前缀和后缀,特别适合处理集合或数组的字符串拼接。
构造方法
构造方法 | 说明 |
---|---|
StringJoiner(CharSequence delimiter) | 仅指定分隔符 |
StringJoiner(CharSequence delimiter,CharSequence prefix,CharSequence suffix) | 指定分隔符,前缀和后缀 |
代码示例一(仅使用分隔符):
csharp
StringJoiner stringJoiner = new StringJoiner(",");
stringJoiner.add("java");
stringJoiner.add("go");
stringJoiner.add("c++");
System.out.println(stringJoiner.toString());//java,go,c++
代码示例二(指定分隔符、前缀和后缀):
csharp
StringJoiner stringJoiner = new StringJoiner(",","[","]");
stringJoiner.add("java");
stringJoiner.add("go");
stringJoiner.add("c++");
System.out.println(stringJoiner.toString());//[java,go,c++]
常用方法
方法 | 说明 |
---|---|
add(CharSequence element) | 添加元素 |
merge(StringJoiner other) | 合并另一个StringJoiner |
setEmptyValue(CharSequence emptyValue) | 设置空值时的默认输出 |
length() | 返回当前字符串的长度 |
toString() | 返回拼接后的字符串 |
add()方法
csharp
StringJoiner sj = new StringJoiner("-");
sj.add("2023").add("10").add("25");
System.out.println(sj.toString()); // 输出:2023-10-25
merge()方法
csharp
StringJoiner sj1 = new StringJoiner(",", "[", "]");
sj1.add("A").add("B");
StringJoiner sj2 = new StringJoiner("-", "(", ")");
sj2.add("C").add("D");
sj1.merge(sj2); // 合并 sj2 到 sj1
System.out.println(sj1); // 输出:[A,B,C-D]
setEmptyValue()方法
csharp
StringJoiner sj = new StringJoiner(",");
sj.setEmptyValue("(空)"); // 如果没有添加元素,返回默认值
System.out.println(sj.toString()); // 输出:(空)
sj.add("Hello");
System.out.println(sj.toString()); // 输出:Hello
与String.join()对比
String.join()是StringJoiner的简化版本,适用于简单拼接:
ini
List<String> languages = List.of("Java", "Python", "C++");
String result = String.join(" | ", languages);
System.out.println(result); // 输出:Java | Python | C++
但StringJoiner更灵活,支持前缀,后缀和动态添加元素
String.join()底层就是使用StringJoiner
在Stream中使用StringJoiner
Collectors.joining()底层就是StringJoiner
ini
List<String> names = List.of("Alice", "Bob", "Charlie");
String joined = names.stream()
.collect(Collectors.joining(", ", "[ ", " ]"));
System.out.println(joined); // 输出:[ Alice, Bob, Charlie ]
总结
场景 | 推荐方式 |
---|---|
简单拼接 | String.join() |
需要前缀后缀 | StringJoiner |
Steram拼接 | Collectors.joining() |
动态拼接 | StringJoiner |
StringJoiner比StringBuilder更适用于结构化拼接(如CVS、JSON数组等),代码更简洁
字符串底层相关原理
字符串存储的原理
- 直接赋值会复用字符串常量池中的
- new出来的不会复用,而是开辟一个新的空间
==比较号比较的到底是什么?
- 基本类型数据比较的是数据值
- 引用数据类型比较的是地址值
字符串拼接的底层原理
"+"运算符拼接:
对于常量字符串拼接,编译器会在编译器直接合并(编译期优化)
arduino
// 编译前
String s = "a" + "b" + "c";//编译后直接合并"abc"
对于变量拼接,java1.5+会直接转换为StringBuilder
scss
// 编译前
String s1 = str1 + str2 + str3;
// 编译后(Java 1.5+)
String s = new StringBuilder().append(str1).append(str2).append(str3).toString();
性能问题:
在循环中使用+拼接会导致大量临时StringBuilder对象的创建
ini
// 错误示范 - 每次循环都新建 StringBuilder
String result = "";
for (int i = 0; i < 100; i++) {
result += i; // 等同于 result = new StringBuilder(result).append(i).toString();
}
StringBuilder和StringBuffer
go
StringBuilder sb = new StringBuilder();
sb.append("Hello").append(" ").append("World");
String result = sb.toString();
底层原理:
- 基于可变的char数组(JDK9+改为byte数组+编码标记)
- 默认初始容量为16字符,不够时自动扩容(2倍+2)
- StringBuffer是线程安全版(方法加synchronized
扩容机制:
ini
// 伪代码展示扩容逻辑
if (需要的新容量 > 当前容量) {
int newCapacity = (当前容量 << 1) + 2; // 2倍+2
if (newCapacity < 需要的新容量) {
newCapacity = 需要的新容量;
}
// 创建新数组并拷贝数据
char[] newValue = new char[newCapacity];
System.arraycopy(旧数组, 0, 新数组, 0, 当前长度);
StringJoiner原理
ini
StringJoiner sj = new StringJoiner(",");
sj.add("A").add("B");
String result = sj.toString(); // "A,B"
底层实现:
- 内部使用StringBuilder拼接
- 维护分隔符、前缀、后缀等状态
- add()方法会智能处理分隔符的添加
scss
// StringJoiner 核心实现(简化版)
public StringJoiner add(CharSequence element) {
if (第一次添加) {
builder.append(prefix).append(element);
} else {
builder.append(delimiter).append(element);
}
return this;
}
concat()方法
ini
String s = "Hello".concat(" World");
底层原理:
- 每次调用都会创建新的String对象
- 底层使用Arrays.copyOf()和System.arraycopy()
- 性能不如StringBuilder连续拼接
JDK9+的改进
JDK9引入了紧凑字符串(Compact Strings)
- 不再总是使用char数组(2字节/字符)
- 根据内容自动选择:
- Latin-1字符(1字节/字符)
- UTF-16字符(2字节/字符)
- StringBuilder也改为使用byte数组
最佳实践:
- 单行简单拼接:使用
+
或String.concat()
- 循环或多次:使用
StringBuilder
- 结构化拼接(CVS/JSON/SQL):使用
StringJoiner
- 线程安全场景:使用
StringBuffer
- 集合转字符串:使用
String.join()
或Collectors.joining()