1.1核心知识
1.1.1 数据类型
Java中的数据类型可以分为两大类:基本数据类型(Primitive Data Types)和引用数据类型(Reference Data Types)
基本数据类型
基本数据类型是Java语言中内置的、不可再分的数据类型。Java提供了八种基本数据类型:
1.整数类型:
byte
:8位有符号整数,范围从 -128 到 127。short
:16位有符号整数,范围从 -32,768 到 32,767。int
:32位有符号整数,范围从 -2^31 到 2^31 - 1。long
:64位有符号整数,范围从 -2^63 到 2^63 - 1。2.浮点类型:
float
:32位单精度浮点数。double
:64位双精度浮点数。3.字符类型:
char
:16位无符号Unicode字符,范围从 '\u0000' 到 '\uffff'。4.布尔类型:
boolean
:表示逻辑值true
或false
。
引用数据类型
引用数据类型是基于基本数据类型创建的,它们指向对象的内存地址。引用数据类型包括:
1.类(Class):类是创建对象的蓝图或模板。
2.接口(Interface):接口定义了一组方法,但不提供方法的具体实现。
3.数组(Array):数组是一种数据结构,用于存储固定大小的同类型元素序列。
4.枚举(Enum):枚举是一种特殊的数据类型,它包含一组固定的常量。
5.注解(Annotation):注解是一种用于提供元数据的标签。
6.字符串(String):虽然
String
类型在Java中是通过java.lang.String
类实现的,但它通常被视为基本数据类型,因为它在Java中非常常用且特殊。
注意事项
- 基本数据类型的变量直接存储值,而引用数据类型的变量存储的是对象的引用(内存地址)。
- 基本数据类型在声明时会自动初始化为默认值,而引用数据类型在声明时默认为
null
。- 基本数据类型没有继承关系,而类和接口可以继承其他类和接口。
在Java中,数据类型转换可以分为两种类型:隐式转换(自动类型转换)和显式转换(强制类型转换)。以下是这两种类型转换的详细说明:
隐式转换(自动类型转换)
隐式转换发生在当一个较小的数据类型被赋值给一个较大类型的数据类型时。这种转换是自动进行的,不需要程序员显式地进行转换操作。例如:
int i = 10;
long l = i; // 隐式转换,将int类型自动转换为long类型
在隐式转换中,数据类型转换遵循以下规则:
- 从
byte
、short
、char
类型到int
类型的转换是安全的。 - 从
int
类型到long
、float
或double
类型的转换也是安全的。 - 从
long
类型到float
或double
类型的转换是安全的。 - 从
float
类型到double
类型的转换是安全的。
显式转换(强制类型转换)
显式转换发生在当一个较大类型的数据类型需要赋值给一个较小类型的数据类型时。这种转换需要程序员显式地进行类型转换操作,因为可能会导致精度损失或数据溢出。例如:
double d = 10.5;
int i = (int) d; // 显式转换,将double类型强制转换为int类型
在显式转换中,需要使用类型转换操作符(如 (int)
),并且需要程序员确保转换后的值在目标数据类型的范围内。如果转换后的值超出了目标数据类型的范围,可能会导致数据丢失或异常。
注意事项
- 在进行显式转换时,如果转换后的值超出了目标数据类型的范围,可能会导致数据丢失或异常。
- 在进行显式转换时,如果目标数据类型无法表示源数据类型的值,可能会导致精度损失。
- 在进行显式转换时,如果源数据类型和目标数据类型不兼容,可能会导致编译错误。
- 在进行显式转换时,如果源数据类型是
double
或float
,目标数据类型是int
或long
,则需要使用类型转换操作符。
1.1.2 常量和变量
在Java中,常量和变量是两种不同的数据存储方式,它们在声明和使用上有不同的规则和特点。
常量(Constants)
常量是固定不变的值,一旦赋值后,其值不能被修改。在Java中,常量通常使用 final
关键字来声明。
public class ConstantsExample {public static final double PI = 3.14159; // 声明一个常量 PIpublic static void main(String[] args) {System.out.println("The value of PI is: " + PI);// PI = 3.14; // 错误:不能修改常量的值}
}
在上面的例子中,PI
被声明为 final
,这意味着它是一个常量,其值在程序运行期间不能被改变。
变量(Variables)
变量是存储数据的容器,其值可以在程序运行期间被修改。在Java中,变量必须先声明后使用,并且在使用前必须初始化。
public class VariablesExample {public static void main(String[] args) {int number = 10; // 声明并初始化一个变量 numbernumber = 20; // 修改变量 number 的值System.out.println("The value of number is: " + number);}
}
在上面的例子中,number
是一个变量,它的值在程序运行期间可以被修改。
常量和变量的区别
- 可变性:常量的值在声明后不能被修改,而变量的值可以在程序运行期间被修改。
- 声明方式:常量使用
final
关键字声明,而变量不需要。 - 初始化:常量在声明时必须初始化,而变量可以在声明时或在程序的其他部分初始化。
- 作用域:常量的作用域可以是类级别(
static final
),也可以是实例级别(非静态final
),而变量的作用域可以是局部的(在方法内部),也可以是类级别或实例级别。
正确使用常量和变量可以提高代码的可读性和可维护性。常量通常用于表示那些在程序中不会改变的值,如数学常数、配置参数等。变量则用于存储程序运行过程中需要改变的数据。
变量的命名规则
1.首字母:变量名必须以字母(a-z
或 A-Z
)、美元符号($
)或下划线(_
)开头。数字不能作为变量名的首字母。
2.后续字符:变量名的后续字符可以是字母、数字、美元符号或下划线。
3.长度:变量名可以包含任意数量的字符,但Java编译器会忽略超过65535个字符的变量名。
4.关键字:变量名不能是Java语言的关键字或保留字。
5.区分大小写:Java是区分大小写的语言,因此myVariable
和myvariable
被视为不同的变量。
6.有意义的命名:变量名应该具有描述性,能够清楚地表达变量的用途。
7.避免下划线和美元符号:虽然允许使用下划线和美元符号,但通常建议避免在变量名中使用它们,除非它们有助于提高可读性。
8.避免使用连字符:变量名中不能包含连字符(-
),但可以使用下划线(_
)。
9.避免使用中文字符:虽然Java允许使用中文字符作为变量名,但为了代码的可读性和国际化,建议避免使用。
变量的分类
在Java中,根据作用域的不同,变量可以分为局部变量和成员变量两大类:
局部变量(Local Variables)
局部变量是在方法、构造函数或代码块内部声明的变量。它们的作用域仅限于声明它们的代码块内。局部变量在使用前必须显式初始化。
- 方法局部变量:在方法内部声明的变量。
- 构造函数局部变量:在构造函数内部声明的变量。
- 代码块局部变量:在任何代码块(如
if
、for
、while
语句的花括号内)内部声明的变量。
public class MyClass {public void myMethod() {int localVar = 10; // 方法局部变量// ...}public MyClass() {int constructorVar = 20; // 构造函数局部变量// ...}public void myOtherMethod() {if (true) {int blockVar = 30; // 代码块局部变量// ...}}
}
成员变量(Member Variables)
成员变量是在类内部声明的变量,它们不属于任何方法或构造函数。成员变量可以是实例变量或类变量。
- 实例变量:在类内部,但不在任何方法、构造函数或代码块内部声明的变量。每个类的实例都有自己的实例变量副本。
- 类变量(静态变量):使用
static
关键字声明的变量。类变量属于类本身,而不是类的实例,因此它们在类加载时被初始化,并且在所有实例之间共享。
public class MyClass {int instanceVar; // 实例变量static int classVar; // 类变量// ...
}
1.1.3 运算符和表达式
算术运算符、关系运算符、逻辑运算符、位运算符、赋值运算符、条件运算符、类型运算符、其他运算符
1.1.4 流程控制语句
Java流程控制语句用于控制程序的执行流程,包括顺序执行、条件判断和循环。以下是Java中常用的流程控制语句:
顺序执行
顺序执行是最基本的流程控制,按照代码的编写顺序依次执行。
int x = 1;
int y = 2;
int z = x + y; // 顺序执行
条件判断
条件判断用于根据条件执行不同的代码块。
if 语句
int score = 85;
if (score >= 90) {System.out.println("优秀");
} else if (score >= 80) {System.out.println("良好");
} else {System.out.println("及格");
}
switch 语句
int month = 3;
switch (month) {case 1:case 12:System.out.println("冬季");break;case 3:case 4:case 5:System.out.println("春季");break;// 其他case...default:System.out.println("未知月份");
}
循环
循环用于重复执行一段代码,直到满足某个条件。
for 循环
for (int i = 0; i < 5; i++) {System.out.println("循环次数: " + i);
}
while 循环
int i = 0;
while (i < 5) {System.out.println("循环次数: " + i);i++;
}
do-while 循环
int i = 0;
do {System.out.println("循环次数: " + i);i++;
} while (i < 5);
跳转语句
跳转语句用于改变程序的执行流程。
break 语句
for (int i = 0; i < 10; i++) {if (i == 5) {break; // 跳出循环}System.out.println(i);
}
continue 语句
for (int i = 0; i < 10; i++) {if (i % 2 == 0) {continue; // 跳过当前循环的剩余部分}System.out.println(i);
}
return 语句
public int add(int a, int b) {return a + b; // 返回方法的结果并结束方法
}
1.2 面向对象
1.2.1 面向对象的三大特征
封装(Encapsulation):
封装是指将对象的状态(属性)和行为(方法)包装在一起,形成一个独立的对象,并对对象的内部实现细节进行隐藏。通过访问修饰符(如 private
、protected
、public
)来控制类的成员(属性和方法)的访问级别,从而保护数据不被外部随意访问和修改。
继承(Inheritance):
继承是面向对象编程中的一个特性,它允许创建一个类(子类)来继承另一个类(父类)的属性和方法。子类可以扩展父类的功能,也可以覆盖父类的方法。继承是代码复用的一种方式,它有助于减少代码的冗余,提高开发效率。
多态(Polymorphism):
多态是指允许不同类的对象对同一消息做出响应。在Java中,多态主要通过方法重载(Overloading)和方法重写(Overriding)来实现。方法重载允许在同一个类中定义多个同名方法,但它们的参数列表不同。方法重写允许子类提供特定于自己的实现,覆盖父类的方法。多态性使得程序更加灵活,易于扩展和维护。
这三大特征是面向对象编程的核心,它们共同构成了面向对象编程的基础。通过封装,可以隐藏对象的内部实现细节,提高代码的安全性和可维护性;通过继承,可以实现代码的复用,减少重复代码;通过多态,可以提高代码的灵活性和可扩展性。
1.2.2 类和对象
类(Class):
- 类是创建对象的蓝图或模板。
- 类定义了对象的属性(Attributes)和行为(Behaviors),即对象的状态和可以执行的操作。
- 类可以包含方法(Methods)和变量(Fields)。
- 类可以有构造函数(Constructors),用于创建对象时初始化对象的状态。
- 类可以有静态成员(Static Members),这些成员属于类本身,而不是类的任何特定实例。
在Java中,类的声明和类的方法是面向对象编程(OOP)的两个核心概念。下面我将分别解释这两个概念。
类的声明
类的声明定义了一个新的数据类型,它包含了数据和操作这些数据的方法。类的声明通常包括以下部分:
1.访问修饰符:如public
、private
等,用于控制类的访问权限。
2.类名:类的名称,遵循Java的命名规则。
3.类体:包含类的属性(成员变量)和方法(成员函数)的定义。
格式如下:
[标识符] class 类名称
{
//类的成员变量
//类的方法
}
下面是一个简单的类声明的例子:
public class Person {// 类的属性(成员变量)private String name;private int age;// 类的构造函数public Person(String name, int age) {this.name = name;this.age = age;}// 类的方法(成员函数)public void introduce() {System.out.println("Hello, my name is " + name + " and I am " + age + " years old.");}// Getter和Setter方法public String getName() {return name;}public void setName(String name) {this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}
}
类的方法
类的方法定义了类可以执行的行为。方法可以接受参数,执行操作,并返回结果。方法可以是实例方法(非静态方法)或静态方法(类方法)。
1.实例方法:需要通过类的实例来调用,可以访问类的实例变量和实例方法。
2.静态方法:通过类名直接调用,不能访问类的实例变量,只能访问类的静态变量和静态方法。
方法的声明包括:
- 访问修饰符:如
public
、private
等。 - 返回类型:方法返回的数据类型,可以是基本数据类型、对象类型或
void
(表示没有返回值)。 - 方法名:方法的名称,遵循Java的命名规则。
- 参数列表:方法接受的输入参数,包括参数类型和参数名。
- 方法体:包含方法的实现代码。
格式如下:
修饰符 返回值类型 方法名称 (参数列表)
{
//方法体
return 返回值;
}
下面是一个方法声明的例子:
public class Calculator {// 静态方法,用于计算两个数的和public static int add(int a, int b) {return a + b;}// 实例方法,用于计算两个数的乘积public int multiply(int a, int b) {return a * b;}
}
对象(Object):
- 对象是类的实例,是类的具体实现。
- 每个对象都有自己的状态,这个状态由对象的属性来表示。
- 对象可以执行类中定义的行为,即调用类的方法。
- 对象是通过类的构造函数创建的,创建对象的过程称为实例化(Instantiation)。
- 声明:声明一个对象,包括对象的名称和对象类型
- 实例化:通过关键字new来创建一个对象
- 初始化:使用new创建对象时,会调用构造方法初始化对象
在java的世界里,“一切皆为对象”,面向对象的核心就是对象,由类产生对象的格式如下:
类名 对象名 = new 类名();
访问对象的成员变量或者方法的格式如下:
对象名称.属性名
对象名称.方法名()
在Java中,构造方法(Constructor)是一种特殊的方法,用于在创建对象时初始化对象。构造方法的名称必须与类名完全相同,并且它没有返回类型,甚至没有void
。构造方法的主要目的是为对象的属性设置初始值。
构造方法可以有参数,这允许您在创建对象时传递不同的值来初始化对象的属性。如果一个类没有显式定义构造方法,Java编译器会自动提供一个无参的默认构造方法。
下面是一个带有构造方法的简单Java类的例子:
public class Person {// 类的属性(成员变量)private String name;private int age;// 无参构造方法public Person() {// 在这里可以设置默认值或执行其他初始化操作}// 带参数的构造方法public Person(String name, int age) {this.name = name; // 使用this关键字来区分成员变量和参数this.age = age;}// 类的其他方法public void introduce() {System.out.println("Hello, my name is " + name + " and I am " + age + " years old.");}// Getter和Setter方法public String getName() {return name;}public void setName(String name) {this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}
}
在这个例子中,Person
类有两个构造方法:
1.无参构造方法:它没有参数,如果在创建Person
对象时没有提供任何参数,那么这个构造方法会被调用。
2.带参数的构造方法:它接受两个参数,name
和age
,用于初始化对象的name
和age
属性。当创建Person
对象并提供这两个参数时,这个构造方法会被调用。
例如,创建Person
对象的代码可能如下:
public class Main {public static void main(String[] args) {// 使用无参构造方法创建Person对象Person person1 = new Person();person1.introduce();// 使用带参数的构造方法创建Person对象Person person2 = new Person("Alice", 30);person2.introduce();}
}
在这个main
方法中,person1
对象使用无参构造方法创建,而person2
对象使用带参数的构造方法创建,并且提供了name
和age
的值。
1.2.3 抽象类和抽象方法
在Java中,抽象类和抽象方法是面向对象编程(OOP)中的两个概念,它们用于定义一个类的蓝图,但不提供完整的实现。抽象类和抽象方法允许子类继承和实现它们。
抽象类
抽象类是一种不能被实例化的类,它可能包含抽象方法和具体方法。抽象类使用abstract
关键字来声明。
抽象类的特点包括:
- 不能被实例化。
- 可以包含抽象方法和具体方法。
- 可以包含构造方法,但不能直接实例化。
- 可以包含静态成员和非静态成员。
- 子类必须实现抽象类中的所有抽象方法。
- 抽象类中不一定包含抽象方法,但是由抽象方法的类必定是抽象类。
- 抽象类中的抽象方法只是声明,不包含方法体,就是不给出方法的具体实现也就是不给出方法的具体功能。
- 构造方法、类方法(用static修饰的方法)不能声明为抽象方法
- 抽象类的子类必须给出抽象类中的抽象方法的具体实现,除非改子类也是抽象类。
下面是一个抽象类的例子:
public abstract class Animal {// 抽象方法public abstract void makeSound();// 具体方法public void eat() {System.out.println("Animal is eating.");}
}
抽象方法
抽象方法是一种没有实现的方法,它只有方法签名,没有方法体。抽象方法必须在抽象类中声明,并且使用abstract
关键字。抽象方法强制子类提供具体实现。
抽象方法的实现:
继承抽象类的子类必须重写父类的抽象方法,否则,该子类也必须声明为抽象类。最终,必须有子类实现父类的抽象方法,否则,从最初的父类到最终的子类都不能用来实例化对象。
抽象方法的特点包括:
- 只有方法签名,没有方法体。
- 必须在抽象类中声明。
- 子类必须实现抽象方法。
下面是一个包含抽象方法的抽象类的例子:
public abstract class Shape {// 抽象方法public abstract double getArea();// 抽象方法public abstract double getPerimeter();
}
在这个例子中,Shape
是一个抽象类,它有两个抽象方法:getArea()
和getPerimeter()
。任何继承自Shape
的子类都必须实现这两个方法。
实现抽象类和抽象方法
要使用抽象类和抽象方法,你需要创建一个继承自抽象类的子类,并实现所有抽象方法。下面是一个实现抽象类和抽象方法的例子:
public class Dog extends Animal {@Overridepublic void makeSound() {System.out.println("Dog says: Woof!");}
}public class Circle extends Shape {private double radius;public Circle(double radius) {this.radius = radius;}@Overridepublic double getArea() {return Math.PI * radius * radius;}@Overridepublic double getPerimeter() {return 2 * Math.PI * radius;}
}
在这个例子中,Dog
类继承自Animal
抽象类,并实现了makeSound()
抽象方法。Circle
类继承自Shape
抽象类,并实现了getArea()
和getPerimeter()
抽象方法。这样,Dog
和Circle
类就提供了Animal
和Shape
抽象类的具体实现。
1.2.4 接口
在Java中,接口(Interface)是一种引用类型,它允许开发者定义一组方法,这些方法可以被类实现(implement)。接口可以包含抽象方法(没有方法体的方法)和默认方法(带有方法体的方法)。接口不能被实例化,但可以被实现类实例化。
接口的主要特点包括:
1.抽象方法:接口中的方法默认是抽象的,除非它们被声明为默认方法。抽象方法没有方法体,必须在实现接口的类中被实现。
2.默认方法:接口中的方法可以被声明为默认方法,它们提供了一个默认的实现。默认方法可以被实现类覆盖。
3.静态方法:接口可以包含静态方法,这些方法属于接口本身,而不是接口的任何实现。
4.常量:接口可以包含静态的、最终的(final)变量,这些变量在接口中被初始化后,其值不能被改变。
5.实现:类可以实现一个或多个接口,通过implements
关键字来实现接口中的方法。
6.继承:接口可以继承一个或多个其他接口,通过extends
关键字来继承。
下面是一个接口的例子:
public interface MyInterface {// 抽象方法void abstractMethod();// 默认方法default void defaultMethod() {System.out.println("This is a default method.");}// 静态方法static void staticMethod() {System.out.println("This is a static method.");}// 常量int CONSTANT = 100;
}
在上面的例子中,MyInterface
是一个接口,它包含一个抽象方法abstractMethod
,一个默认方法defaultMethod
,一个静态方法staticMethod
,以及一个常量CONSTANT
。
要实现接口,你需要创建一个类,并使用implements
关键字来实现接口中的所有抽象方法。例如:
public class MyClass implements MyInterface {@Overridepublic void abstractMethod() {System.out.println("Implementation of abstract method.");}// 可以选择性地实现默认方法@Overridepublic void defaultMethod() {System.out.println("Overridden default method.");}
}
在上面的例子中,MyClass
实现了MyInterface
接口,并提供了abstractMethod
的实现。它还覆盖了defaultMethod
的默认实现。
现在,你可以创建MyClass
的实例并调用接口中的方法:
public class Main {public static void main(String[] args) {MyClass myInstance = new MyClass();myInstance.abstractMethod(); // 输出: Implementation of abstract method.myInstance.defaultMethod(); // 输出: Overridden default method.MyInterface.staticMethod(); // 输出: This is a static method.}
}
在上面的main
方法中,我们创建了MyClass
的一个实例,并调用了接口中的方法。注意,静态方法staticMethod
是通过接口名直接调用的,而不是通过实例调用。
接口和抽象类的异同
相同点:
1.不能被实例化:接口和抽象类都不能直接创建对象实例,它们都需要被具体类实现或继承。
2.包含抽象方法:接口和抽象类都可以包含抽象方法,这些方法没有实现,必须在实现类中提供具体实现。
3.提供多态性:接口和抽象类都支持多态性,即可以使用接口或抽象类的引用指向实现类的对象。
都不能被直接实例化,都可以包含抽象方法,派生类必须实现为实现的方法。
不同点:
1.实现方式:
- 接口:使用
interface
关键字定义,可以包含抽象方法和默认方法(带有实现的方法),以及静态方法和常量。 - 抽象类:使用
abstract
关键字定义,可以包含抽象方法和具体方法,以及构造方法和静态方法。
2.继承:
- 接口:可以被多个类实现,一个类可以实现多个接口(多实现)。
- 抽象类:只能被一个类继承,一个类只能继承一个抽象类(单继承)。
3.默认方法:
- 接口:可以包含默认方法,这些方法提供了一个默认的实现,可以被实现类覆盖。
- 抽象类:不能包含默认方法,但可以包含具体方法。
4.构造方法:
- 接口:不能包含构造方法。
- 抽象类:可以包含构造方法,但构造方法不能被直接调用,只能在子类的构造方法中隐式调用。
5.静态方法和常量:
- 接口:可以包含静态方法和常量,这些静态成员属于接口本身,而不是接口的实现类。
- 抽象类:也可以包含静态方法和常量,但它们属于抽象类本身。
6.访问修饰符:
- 接口:接口中的方法默认是
public
的,接口中的变量默认是public static final
的。 - 抽象类:抽象类中的方法和变量可以使用不同的访问修饰符。
7.使用场景:
- 接口:通常用于定义一组方法,这些方法可以被不同的类实现,以实现多态性。例如,Java中的
Comparable
接口。 - 抽象类:通常用于定义一组方法和属性,这些方法和属性可以被子类继承和扩展。例如,一个抽象的基类,它提供了一些通用的方法实现。
在选择使用接口还是抽象类时,通常取决于具体的应用场景和设计需求。如果需要多实现和多继承,通常使用接口;如果需要单继承和提供一些通用的实现,通常使用抽象类。
1.3 精选面试、笔试题解析
1.3.1 java基本数据类型之间如何转换
在Java中,基本数据类型之间的转换可以分为自动转换(隐式转换)和强制转换(显式转换)。
自动转换(隐式转换)
自动转换,也称为隐式转换,发生在当一个较小的数据类型被赋值给一个较大类型的数据类型时。这种转换是自动进行的,不需要程序员显式地进行类型转换。自动转换遵循以下规则:
1.从低精度到高精度:例如,从byte
、short
、char
到int
、long
、float
、double
。
2.从整数类型到浮点类型:例如,从int
到float
或double
。
3.从浮点类型到更高精度的浮点类型:例如,从float
到double
。
自动转换的目的是为了防止数据丢失,确保数据的精度不会因为类型转换而降低。
强制转换(显式转换)
强制转换,也称为显式转换,发生在当一个较大类型的数据类型需要被赋值给一个较小类型的数据类型时。这种转换需要程序员显式地进行类型转换,因为可能会导致数据精度的丢失。强制转换使用括号包围目标类型,并放在要转换的值前面。例如:
double d = 10.5;
int i = (int)d; // 强制转换为int类型,小数部分会被截断
在上面的例子中,d
是一个double
类型的变量,其值为10.5
。当它被强制转换为int
类型时,小数部分0.5
会被截断,只保留整数部分10
。
强制转换时需要注意以下几点:
1.数据丢失:强制转换可能会导致数据精度的丢失,特别是当从浮点类型转换为整数类型时。
2.溢出:如果目标类型无法容纳原始值的范围,可能会发生溢出。
3.精度损失:在转换过程中,可能会丢失小数部分,导致精度损失。
因此,在进行强制转换时,需要确保转换后的值仍然在目标类型的范围内,并且不会丢失过多的精度。如果可能,最好在转换前进行检查,以避免潜在的问题。
1.3.2 谈谈你对面向对象的理解
面向对象编程(Object-Oriented Programming,简称OOP)是一种编程范式,它使用“对象”来设计软件。在面向对象编程中,对象是数据和操作这些数据的方法的集合。面向对象编程的核心概念包括封装、继承、多态和抽象。
封装(Encapsulation)
封装是面向对象编程的基础,它涉及到将数据(属性)和行为(方法)捆绑在一起,形成一个单一的单元,即对象。封装的目的是隐藏对象的内部状态和实现细节,只暴露一个公共的接口供外部使用。这样可以保护数据不被外部代码随意访问和修改,从而提高代码的安全性和可维护性。
继承(Inheritance)
继承是面向对象编程中的一种机制,允许创建一个类(子类)来继承另一个类(父类)的属性和方法。子类可以扩展或修改父类的行为,而不必从头开始编写代码。继承有助于代码的重用和扩展,是面向对象编程中实现“is-a”关系的一种方式。
多态(Polymorphism)
多态是面向对象编程中的一种能力,允许不同的对象以不同的方式响应相同的消息或操作。多态可以通过方法重载(在同一个类中定义多个同名方法,但参数列表不同)和方法重写(子类中定义与父类同名的方法,提供新的实现)来实现。多态是面向对象编程中实现“has-a”关系的一种方式。
抽象(Abstraction)
抽象是面向对象编程中的一种概念,它涉及到从具体的实例中提取出共性,形成一个抽象的概念或模型。抽象可以是类的抽象,也可以是方法的抽象。类的抽象通常通过抽象类或接口来实现,而方法的抽象则通过抽象方法来实现。抽象有助于简化复杂性,使代码更加清晰和易于理解。
面向对象编程的优点
- 代码重用:通过继承和封装,可以重用现有的代码,减少重复工作。
- 易于维护和扩展:封装和抽象使得代码更容易维护和扩展。
- 模块化:面向对象编程鼓励模块化设计,有助于管理复杂性。
- 灵活性和可扩展性:多态性使得系统可以更容易地适应新的需求。
面向对象编程是一种强大的编程范式,它在软件开发中得到了广泛应用。通过使用面向对象的概念,开发者可以创建出更加模块化、可维护和可扩展的软件系统。
1.3.3 Java中的访问修饰符有哪些
Java中的访问修饰符用于控制类、方法和变量的访问级别。Java提供了以下四种访问修饰符:
1.public
:公共访问修饰符。使用public
修饰的类、方法或变量可以被任何其他类访问,无论这些类是否在同一个包中。
2.protected
:受保护访问修饰符。使用protected
修饰的类、方法或变量可以在同一个包内的任何类中访问,也可以在不同包中的子类中访问。
3.default
(无修饰符):默认访问修饰符。如果一个类、方法或变量没有使用任何访问修饰符,那么它具有默认访问级别。默认访问级别的类、方法或变量只能在同一个包内访问。
4.private
:私有访问修饰符。使用private
修饰的类、方法或变量只能在定义它们的类内部访问。
这些访问修饰符的使用可以帮助开发者控制代码的封装性,确保类的内部实现细节不会被外部代码随意访问和修改,从而提高代码的安全性和可维护性。在设计类和接口时,合理地使用访问修饰符是非常重要的。
注意:protected修饰符所修饰的类属于成员变量和方法,只可以被子类访问,而不管子类是不是和父类位于同意个包中。default修饰符所修饰的类也属于成员变量和方法,但只可被同一个包中的其他类访问,而不管其他类是不是该类的子类。protected属于子类限制修饰符,而default属于包限制修饰符。
1.3.4 重载和重写
方法的重载(Overloading)和重写(Overriding)是面向对象编程中的两个重要概念,它们都涉及到方法的多态性,但它们的目的和实现方式有所不同。
方法的重载(Overloading):
重载是指在同一个类中可以存在多个同名方法,只要它们的参数列表不同即可。参数列表不同可以是参数的个数不同,参数的类型不同,或者参数的顺序不同。重载使得同一个方法可以根据不同的参数列表执行不同的操作。
例如:
public class Example {// 重载方法1:参数个数不同public void display(int a) {// ...}// 重载方法2:参数类型不同public void display(String a) {// ...}// 重载方法3:参数顺序不同public void display(String a, int b) {// ...}
}
方法的重写(Overriding):
重写是指子类提供一个与父类中同名方法的新实现,这个新实现会覆盖父类中的方法。重写通常用于实现多态性,即根据对象的实际类型来调用相应的方法。重写的方法必须有相同的名称、参数列表和返回类型(或子类型)。
例如:
class Parent {public void display() {System.out.println("Parent display method");}
}class Child extends Parent {@Overridepublic void display() {System.out.println("Child display method");}
}
重载和重写的区别:
1.目的:
- 重载的目的是提供方法的多种实现,通过不同的参数列表来区分。
- 重写的目的是在子类中提供一个与父类同名方法的新实现,以实现多态性。
2.发生范围:
- 重载发生在同一个类中。
- 重写发生在子类和父类之间。
3.参数列表:
- 重载要求方法的参数列表必须不同。
- 重写要求方法的参数列表必须相同。
4.返回类型:
- 重载对返回类型没有要求,可以相同也可以不同,但不能仅通过返回类型来区分重载。
- 重写要求返回类型相同或者是返回类型的子类型。
5.访问修饰符:
- 重载对访问修饰符没有特殊要求。
- 重写时,子类方法的访问修饰符不能比父类方法的访问修饰符更严格。
理解重载和重写的区别对于编写清晰、可维护的面向对象代码非常重要。在实际编程中,合理地使用这两种机制可以提高代码的灵活性和可扩展性。
1.3.5 什么是构造方法
构造方法(Constructor)是Java中用于初始化对象的一种特殊方法。它在创建对象时被自动调用,用于为对象的实例变量赋予初始值。构造方法与类同名,并且没有返回类型,甚至连void
也不返回。
构造方法的语法规则:
1.构造方法的名称必须与类名完全相同。
2.构造方法没有返回类型,连void
也不写。
3.构造方法可以有参数列表,用于接收传递给构造方法的参数。
4.构造方法可以有多个,这称为构造方法的重载。
5.构造方法可以使用访问修饰符,如public
、protected
、private
或默认访问修饰符(无修饰符),以控制构造方法的可见性。
构造方法的特点:
1.自动调用:当使用new
关键字创建对象时,构造方法会被自动调用。
2.初始化:构造方法用于初始化对象的状态,即为对象的实例变量赋予初始值。
3.无返回值:构造方法没有返回值,不能用return
语句返回任何值。
4.重载:一个类可以有多个构造方法,只要它们的参数列表不同即可。
5.默认构造方法:如果一个类没有显式定义任何构造方法,Java编译器会自动提供一个无参的默认构造方法,该构造方法不做任何操作。
6.父类构造方法:如果子类没有显式调用父类的构造方法,Java编译器会自动插入对父类无参构造方法的调用。如果父类没有无参构造方法,子类必须显式调用父类的某个构造方法。
示例:
public class MyClass {private int number;private String name;// 无参构造方法public MyClass() {number = 0;name = "Default Name";}// 带参数的构造方法public MyClass(int number, String name) {this.number = number;this.name = name;}// ... 其他方法 ...
}
在上面的例子中,MyClass
有两个构造方法:一个无参构造方法和一个带参数的构造方法。无参构造方法将number
设置为0,将name
设置为"Default Name"。带参数的构造方法则允许在创建对象时指定number
和name
的值。
构造方法是面向对象编程中的一个核心概念,它使得对象的创建和初始化更加灵活和可控。
1.3.6 局部变量与成员变量有什么区别
局部变量是指在方法或者方法代码块中定义的变量,其作用域是其所在的代码块。
成员变量是指在类的体系和变量部分中定义的变量
局部变量和成员变量的区别:
(1)定义的位置
局部变量:定义在方法的内部。
成员变量:定义在方法的外部,即直接写再类中。
(2)作用范围
局部变量:只适用于方法中,描述类的公共属性。
成员变量:整个类中都可以通用。
(3)默认值(初始化)
局部变量:没有默认初始值,需要手动进行复制之后才能使用。
成员变量:有默认初始值,如int类型的默认值为0;float类型的默认值为0.0f;double类型的默认值为0.0.
(4)内存的位置
局部变量:位于栈内存
成员变量:位于堆内存
(5)声明周期
局部变量:在调用对应的方法时,局部变量因为执行创建语句而存在,超出自己的作用域之后会立即从内存消失。
成员变量:成员变量随着对象的创建而创建,随着对象的消失而消失。
1.3.7 解释一下 break、continue 以及 return 的区别
break
、continue
和return
是Java中用于控制循环和方法执行流程的关键字。它们在不同的上下文中有着不同的用途和行为。
break:
break
关键字用于立即终止包含它的最内层循环的执行。- 当
break
语句执行时,它会跳出当前循环,不再执行循环体中break
之后的任何语句。 break
通常用于提前退出循环,当满足某个条件时不再继续执行循环。
continue:
continue
关键字用于跳过当前循环的剩余部分,并开始下一次循环迭代。- 当
continue
语句执行时,它会跳过当前循环的剩余代码,直接进行下一次循环的条件判断。 continue
通常用于跳过当前迭代中不需要执行的代码,但仍然希望继续执行循环。
return:
return
关键字用于从当前方法返回,并且可以返回一个值(如果方法声明了返回类型)。- 当
return
语句执行时,它会结束当前方法的执行,并将控制权返回给调用该方法的代码。 return
可以用于方法的任何位置,包括在循环内部,但一旦执行,它将结束整个方法的执行,而不是仅仅结束循环。
区别:
break
和continue
是循环控制语句,它们只影响循环的执行流程,而return
是方法控制语句,它影响整个方法的执行流程。break
用于完全终止循环,而continue
用于跳过当前迭代,继续执行下一次迭代。return
不仅结束循环,还结束整个方法的执行,并且可以返回一个值。
示例:
public class ControlFlowExample {public static void main(String[] args) {for (int i = 0; i < 10; i++) {if (i == 5) {break; // 当i等于5时,终止循环}if (i % 2 == 0) {continue; // 当i为偶数时,跳过当前迭代}System.out.println(i); // 只有当i为奇数且不等于5时,才会打印}int result = sum(10); // 调用方法并接收返回值System.out.println("Sum is: " + result);}public static int sum(int n) {int sum = 0;for (int i = 1; i <= n; i++) {sum += i;if (sum > 50) {return sum; // 当累加和超过50时,返回当前累加和并结束方法}}return sum; // 如果没有提前返回,最终返回累加和}
}
在这个示例中,break
用于在i
等于5时终止循环,continue
用于在i
为偶数时跳过当前迭代,而return
用于在累加和超过50时结束sum
方法的执行,并返回当前的累加和。
1.3.8 Java中的基本数据类型有哪些
Java中的基本数据类型(Primitive Data Types)是Java语言中内置的、不可再分的数据类型。它们是Java语言的基石,用于存储数值、字符和布尔值等基本数据。Java中的基本数据类型包括以下八种:
1.整数类型:
byte
:占用1个字节(8位),取值范围是-128到127。short
:占用2个字节(16位),取值范围是-32,768到32,767。int
:占用4个字节(32位),取值范围是-2^31到2^31-1(大约是-2.15亿到2.15亿)。long
:占用8个字节(64位),取值范围是-2^63到2^63-1。
2.浮点类型:
float
:占用4个字节(32位),用于表示单精度浮点数,取值范围大约是±3.40282347E+38F(6到7位有效数字)。double
:占用8个字节(64位),用于表示双精度浮点数,取值范围大约是±1.79769313486231570E+308(15位有效数字)。
3.字符类型:
char
:占用2个字节(16位),用于表示单个字符,取值范围是0到65,535(Unicode字符集中的字符)。
4.布尔类型:
boolean
:占用1个字节(8位),用于表示逻辑值true
或false
。
在Java中,基本数据类型与对象类型(如String
、Integer
等)不同,它们不是对象,因此没有方法。基本数据类型的变量直接存储值,而不是引用。在Java 5及以后的版本中,基本数据类型可以被包装成相应的对象类型,例如Integer
、Double
等,这些包装类提供了更多的方法和特性。
此外,Java 5引入了自动装箱(autoboxing)和自动拆箱(unboxing)的概念,允许基本数据类型和它们的包装类之间进行隐式转换,这简化了基本数据类型和对象类型之间的操作。
1.3.9 Java中this的用法
在Java中,this
关键字是一个引用变量,它指向当前对象的引用。this
关键字可以用于多种上下文中,以下是一些常见的用法:
1.引用当前对象的成员变量:
当类的成员变量与方法的参数或局部变量同名时,可以使用this
关键字来区分它们。例如:
public class Person {private String name;public Person(String name) {this.name = name; // 使用this来区分成员变量name和构造方法的参数name}
}
2.调用当前对象的其他构造方法:
在一个构造方法中,可以使用this()
来调用同一个类中的另一个构造方法。例如:
public class Person {private String name;private int age;public Person(String name) {this(name, 0); // 调用另一个构造方法}public Person(String name, int age) {this.name = name;this.age = age;}
}
3.返回当前对象的引用:
在方法中,可以使用return this;
来返回当前对象的引用。例如:
public class Person {private String name;public Person setName(String name) {this.name = name;return this; // 返回当前对象的引用}
}
4.区分成员变量和局部变量:
当方法的参数与成员变量同名时,可以使用this
关键字来明确指定使用成员变量。例如:
public class Person {private String name;public void setName(String name) {this.name = name; // 使用this来区分成员变量name和局部变量name}
}
5.在内部类中引用外部类的实例:
当内部类需要引用外部类的实例时,可以使用this
关键字。例如:
public class OuterClass {private String outerName;class InnerClass {private String innerName;public InnerClass(String innerName) {this.innerName = innerName;this.outerName = OuterClass.this.outerName; // 使用OuterClass.this来引用外部类的实例}}
}
在使用this
关键字时,需要注意以下几点:
this
关键字只能在类的非静态方法中使用。在静态方法中,由于静态方法属于类而非对象,因此不能使用this
。this
关键字不能在类定义的外部使用,它只能在类的内部使用。this
关键字不能用于静态上下文中,因为静态方法和静态变量不属于任何对象实例。
正确使用this
关键字可以提高代码的可读性和可维护性。
1.3.10 接口和抽象类
在Java中,接口(Interface)和抽象类(Abstract Class)都是用来定义抽象概念的,它们都不能被实例化,但可以被其他类实现(接口)或继承(抽象类)。尽管它们在某些方面看起来相似,但它们在设计和使用上有一些关键的区别。
接口(Interface):
1.定义:接口是一组方法的声明,没有实现。在Java 8及以后的版本中,接口可以包含默认方法和静态方法,但这些方法必须有实现。
2.实现:类可以实现(implement)一个或多个接口,通过实现接口中声明的方法来提供具体的行为。
3.多重继承:Java不支持类的多重继承,但一个类可以实现多个接口,从而实现类似多重继承的效果。
4.访问修饰符:接口中的方法默认是public
的,接口中的变量默认是public static final
的。
5.默认方法:Java 8引入了默认方法(default methods),允许在接口中提供方法的默认实现。
6.静态方法:Java 8允许在接口中定义静态方法。
7.私有方法:Java 9允许在接口中定义私有方法,这些方法可以被默认方法和静态方法调用。
抽象类(Abstract Class):
1.定义:抽象类可以包含抽象方法和具体方法。抽象方法没有实现,具体方法有实现。
2.继承:抽象类可以被其他类继承,子类可以覆盖(override)抽象类中的抽象方法。
3.访问修饰符:抽象类中的方法和变量可以有各种访问修饰符。
4.构造方法:抽象类可以有构造方法,这些构造方法在子类实例化时被调用。
5.不能实例化:抽象类不能被实例化,必须通过子类来创建对象。
6.抽象方法:抽象类可以包含抽象方法,这些方法必须在子类中被实现。
接口和抽象类的区别:
- 实现方式:接口通过
implements
关键字被实现,抽象类通过extends
关键字被继承。 - 多重继承:接口支持多重继承,抽象类不支持。
- 访问修饰符:接口中的方法默认是
public
的,抽象类中的方法可以有不同访问修饰符。 - 构造方法:抽象类可以有构造方法,接口不能有构造方法。
- 变量:接口中的变量默认是
public static final
的,抽象类中的变量可以有不同访问修饰符。
在设计时,选择使用接口还是抽象类取决于具体的需求和设计模式。通常,如果需要定义一组行为,而这些行为的具体实现可以由不同的类来完成,那么应该使用接口。如果需要定义一个类的骨架,而这个类的具体实现可以由子类来完成,那么应该使用抽象类。
1.4 名企真题解析
1.4.1 值传递和引用传递
在编程中,参数传递是指将数据传递给函数或方法的过程。根据传递的是数据的副本还是数据的引用,参数传递可以分为值传递(Pass by Value)和引用传递(Pass by Reference)。这两种传递方式在不同的编程语言中有所不同,下面以Java为例来解释这两种传递方式:
值传递(Pass by Value):
在Java中,所有的方法参数传递都是值传递。这意味着当一个方法被调用时,实际传递给方法的是参数的值的副本,而不是参数本身。如果参数是基本数据类型(如int
、double
、boolean
等),那么传递的就是这个基本数据类型的值。如果参数是对象的引用(即对象的内存地址),那么传递的也是这个引用的副本。
在值传递的情况下,方法内部对参数的修改不会影响到方法外部的原始变量。例如:
public class ValuePassingExample {public static void main(String[] args) {int number = 10;System.out.println("Before method call: " + number); // 输出 10increment(number);System.out.println("After method call: " + number); // 输出 10}public static void increment(int num) {num = num + 1;}
}
在这个例子中,increment
方法接收一个int
类型的参数num
,它是一个值传递。即使在方法内部将num
的值增加了1,这个改变不会影响到main
方法中的number
变量。
引用传递(Pass by Reference):
在某些语言中,如C++,可以实现真正的引用传递。在这种情况下,当一个方法被调用时,传递的是参数的内存地址的引用,而不是参数的值。这意味着方法内部对参数的修改会直接影响到方法外部的原始变量。
在Java中,虽然没有真正的引用传递,但是可以通过传递对象的引用(即对象的内存地址)来模拟引用传递的效果。例如:
public class ReferencePassingExample {public static void main(String[] args) {MyClass obj = new MyClass();System.out.println("Before method call: " + obj.value); // 输出 0increment(obj);System.out.println("After method call: " + obj.value); // 输出 1}public static void increment(MyClass obj) {obj.value = obj.value + 1;}
}class MyClass {int value = 0;
}
在这个例子中,increment
方法接收一个MyClass
类型的参数obj
,它是一个引用传递。即使在方法内部将obj.value
的值增加了1,这个改变会直接影响到main
方法中的obj
对象的value
属性。
总结来说,在Java中,所有的方法参数传递都是值传递,但是可以通过传递对象的引用来模拟引用传递的效果。在值传递中,方法内部对参数的修改不会影响到方法外部的原始变量;而在引用传递中,方法内部对参数的修改会直接影响到方法外部的原始变量。
1.4.2 什么是类的反射机制
类的反射机制(Reflection)是Java语言的一个特性,它允许程序在运行时访问和操作类的属性、方法和构造函数等信息。通过反射,可以实现以下功能:
1.检查类的结构:可以获取类的名称、父类、接口、字段(成员变量)、方法、构造函数等信息。
2.创建对象:可以动态地创建类的实例,即使在编译时不知道具体的类名。
3.调用方法:可以调用类的任何方法,包括私有方法。
4.访问字段:可以访问和修改类的任何字段,包括私有字段。
5.动态代理:可以创建一个代理对象,该对象在运行时可以拦截方法调用,并在调用前后执行自定义的代码。
6.动态加载类:可以在运行时动态加载类,这在实现插件系统或模块化系统时非常有用。
在Java中,反射是通过java.lang.reflect
包中的类来实现的,如Class
、Method
、Field
、Constructor
等。以下是一些使用反射的基本步骤:
1.获取Class
对象:可以通过调用Class.forName()
方法来获取一个类的Class
对象,或者通过类名直接调用.class
属性。
2.获取类的成员信息:使用Class
对象的getFields()
、getMethods()
、getConstructors()
等方法来获取类的字段、方法和构造函数。
3.创建对象:使用Class
对象的newInstance()
方法来创建类的实例。
4.调用方法:使用Method
对象的invoke()
方法来调用方法。
5.访问字段:使用Field
对象的get()
和set()
方法来获取和设置字段的值。
下面是一个简单的反射示例:
import java.lang.reflect.Method;public class ReflectionExample {public static void main(String[] args) {try {// 获取Class对象Class<?> clazz = Class.forName("java.lang.String");// 获取方法Method lengthMethod = clazz.getMethod("length");// 创建String对象Object str = "Hello, Reflection!";// 调用方法int length = (int) lengthMethod.invoke(str);System.out.println("The length of the string is: " + length);} catch (Exception e) {e.printStackTrace();}}
}
在这个例子中,我们使用反射来获取String
类的length()
方法,并调用它来获取字符串的长度。
反射机制虽然功能强大,但也有其缺点,比如性能开销较大,因为需要在运行时动态地解析类的信息。因此,应该谨慎使用反射,只在确实需要动态操作类时才使用它。
1.4.3 Java创建对象的方式有几种
在Java中,创建对象通常有以下几种方式:
1.使用new
关键字:
这是最常见的创建对象的方式。通过new
关键字调用类的构造方法来创建对象实例。例如:
MyClass obj = new MyClass();
2.使用反射:
通过Java的反射机制,可以使用Class
类的newInstance()
方法来创建对象。例如:
MyClass obj = MyClass.class.newInstance();
或者使用Constructor
类的newInstance()
方法:
Constructor<MyClass> constructor = MyClass.class.getConstructor();
MyClass obj = constructor.newInstance();
3.使用克隆:
如果一个类实现了Cloneable
接口,那么可以使用clone()
方法来创建对象的副本。例如:
MyClass obj = new MyClass();
MyClass clonedObj = obj.clone();
4.使用反序列化:
当对象被序列化到文件或通过网络传输后,可以使用反序列化来创建对象。例如:
MyClass obj = (MyClass) SerializationUtils.deserialize(serializedBytes);
5.使用工厂方法:
类可以提供一个静态工厂方法来创建对象。例如:
public class MyClass {private MyClass() {// 私有构造方法}public static MyClass createInstance() {return new MyClass();}
}MyClass obj = MyClass.createInstance();
6.使用容器:
在某些框架中,如Spring,可以通过容器来管理对象的创建和生命周期。例如:
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
MyClass obj = context.getBean(MyClass.class);
7.使用依赖注入:
在Spring框架中,可以使用依赖注入来创建对象。例如:
@Component
public class MyClass {// ...
}@Autowired
private MyClass myClass;
每种创建对象的方式都有其适用的场景和优缺点。例如,使用new
关键字是最直接的方式,但可能不够灵活;使用反射可以创建类的实例,即使类是私有的或构造方法是私有的;使用克隆可以创建对象的副本;使用反序列化可以恢复之前序列化的对象;工厂方法和容器提供了更灵活的创建对象的方式,可以用于实现设计模式如工厂模式和单例模式。