第一章、Java帮助文本与核心类库
java类库一般都包括三个部分
- 源码:可以看源码来理解程序
- 字节码:程序开发的过程使用的就是字节码文件
- 帮助文档:对开发提供帮助
注意版本统一
第二章、OCP原则
应该通过添加新的代码来扩展现有代码,而不是修改现有代码
首选多态语法
程序编程中的OCP原则是指“开放/关闭原则”(Open/Closed Principle),是SOLID原则中的一个。OCP原则的定义是:软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。这意味着在不修改现有代码的情况下,可以通过添加新的功能来扩展现有代码。
具体而言,OCP原则要求在设计和实现软件时,应该将不同的职责分离成不同的模块或组件。这样,当需要添加新的功能时,可以通过创建新的模块或组件来扩展现有代码,而不必修改现有代码。这种做法有助于提高代码的可维护性、可扩展性和可重用性。
一、开放性(Open for Extension)
软件实体应该允许在不修改其现有代码的情况下进行扩展。这意味着可以通过添加新的代码来增加功能,而不会影响已经存在的代码。
二、封闭性(Closed for Modification)
一旦软件实体的行为被确定,就应该尽量避免修改其现有的代码。这是为了防止已经存在的代码被意外破坏,从而提高代码的稳定性和可靠性。
例如,在面向对象编程中,可以通过使用接口和抽象类来实现OCP原则。一个类实现了一个接口或继承了一个抽象类,可以保证在不修改原有代码的情况下,对其进行扩展和改进。
总之,OCP原则是一种重要的软件设计原则,它有助于提高代码的质量和可维护性。
第三章、抽象类与抽象方法
一、抽象类概述
- 类到对象是实例化。对象到类是抽象。
- 抽象类 --实例化--》类,类 --抽象--》 抽象类
1、概念
- 类和类之间具有共同特征,将这些共同特征提取出来,形成的就是抽象类。
- 类本身是不存在的,所以抽象类无法创建对象《无法实例化》。
- 抽象类也属于引用数据类型
- java 中所有的类,包括抽象类,都继承自 Object 类
- 抽象类是无法实例化的,无法创建对象的,所以抽象类是用来被子类继承的,抽象类只能作为父类。
- final和abstract不能联合使用,这两个关键字是对立的
-
final
关键字用来修饰一个变量、方法或类。对于一个final
修饰的类,是不能被继承的,而抽象类不能实例对象,要被子类继承,所以会语法冲突
-
- 抽象类的子类可以是抽象类也可以是非抽象类。
- 抽象类虽然无法实例化,但是抽象类有构造方法,子类构造方法中默认的 super()来调用父类中的构造方法
抽象类概念图示:
2、定义语法
【访问权限控制符列表】 abstract class 类名{
类体;
}
package 抽象类与抽象方法;
public abstract class AbstractTest01 {
}
3、抽象类中可以有的内容
1、实例变量(没有abstract修饰符)
2、实例方法(没有abstract修饰符)
3、构造方法(没有abstract修饰符)
4、静态变量(没有abstract修饰符)
5、静态方法(没有abstract修饰符)
6、抽象方法:没有方法体的实例方法(有abstract修饰符)
package 抽象类与抽象方法;
public abstract class AbstractTest01 {
//实例变量
public String instanceVariable;
//实例方法
public void instanceMethod() {
System.out.println("实例方法");
}
//构造方法
public AbstractTest01(String instanceVariable) {
this.instanceVariable = instanceVariable;
System.out.println("构造方法");
}
//静态变量
public static String staticVariable = "静态变量";
//静态方法
public static void staticMethod(){
System.out.println("静态方法");
}
//抽象方法
public abstract void abstractMethod();
public static void main(String[] args) {
//匿名内部类写法
AbstractTest01 abstractTest01 = new AbstractTest01("实例变量") {
@Override
public void abstractMethod() {
System.out.println("抽象方法(没有方法体的实例方法)");
}
};
System.out.println(abstractTest01.instanceVariable);
abstractTest01.instanceMethod();
System.out.println(AbstractTest01.staticVariable);
AbstractTest01.staticMethod();
abstractTest01.abstractMethod();
/**
* 构造方法
* 实例变量
* 实例方法
* 静态变量
* 静态方法
* 抽象方法(没有方法体的实例方法)
*/
}
}
//非抽象子类继承抽象类写法
class ImplementClass extends AbstractTest01{
public ImplementClass(String instanceVariable) {
super(instanceVariable);
}
@Override
public void abstractMethod() {
}
}
如果在抽象类中定义静态变量,那么这个静态变量也是属于类级别的,和普通的类一样。子类继承这个抽象类后,也会继承它的静态变量,可以直接使用类名访问。
下面是一个简单的 Java 抽象类示例,其中定义了一个静态变量:
abstract class Shape {
protected String color;
public static int count = 0;
public Shape(String color) {
this.color = color;
count++;
}
public void setColor(String color) {
this.color = color;
}
public abstract double getArea();
}
在这个例子中,抽象类 Shape
定义了一个静态变量 count
,并在构造函数中对其进行了自增操作。子类可以直接访问这个静态变量,例如:
class Circle extends Shape {
private double radius;
public Circle(String color, double radius) {
super(color);
this.radius = radius;
}
public double getArea() {
return Math.PI * radius * radius;
}
public static void main(String[] args) {
Circle circle = new Circle("red", 5.0);
System.out.println(Shape.count); // 输出 1
}
}
在这个例子中,子类 Circle
直接访问了抽象类 Shape
中定义的静态变量 count
。因为 Circle
类是 Shape
类的子类,所以它可以访问 Shape
类中的所有静态变量和方法。
二、抽象方法概述
1、概念
1、抽象方法表示没有实现的方法,没有方法体的方法。要想继承到非抽象子类中则必须将其实现。
2、可理解为没有方法体的实例方法,例如:public abstract void dosome() :
java语言中凡是没有方法体的方式都是抽象方法吗? 错误
Object类中就有很多方法都没有方法体,都是以“;”结尾的,但他们都不是抽象方法,例如:
public native int hashCode();
//该方法底层调用了C++的动态链接库程序,native表示调用JVM本地程序
2、定义语法
1、没有方法体,以分号结尾。
2、前面修饰符列表中有abstract关键字
3、static和abstract不能联合使用
static
关键字用来修饰静态方法,表示这个方法是属于类而不是对象的。这意味着,可以在不创建类的实例的情况下直接调用静态方法。而且,静态方法可以直接通过类名访问。abstract
关键字用来修饰抽象方法,表示这个方法没有实现,需要在子类中被重写。抽象方法必须被声明在抽象类或接口中,这些类或接口本身是不能被实例化的。- 因为
static
方法是属于类的,而abstract
方法必须在子类中被重写,所以这两个关键字的语义是相互矛盾的。如果在同一个方法中同时使用这两个关键字,就会导致语法错误4、final和abstract不能联合使用
final
关键字用来修饰一个变量、方法或类。对于一个final
方法,它的实现不能被子类重写,因此这个方法的行为是不可变的。而abstract
关键字则用来修饰一个抽象方法,表示这个方法只是一个接口,需要在子类中被实现。- 因为
final
方法的实现是不可变的,而abstract
方法需要在子类中被实现,所以这两个关键字的语义是相互矛盾的。如果在同一个方法中同时使用这两个关键字,就会导致语法错误。5、private和abstract不能联合使用
- private修饰的属性和方法不能被子类所继承,而abstract修饰的抽象方法必须要被非抽象子类重写,重写的前提就是要继承,所以private和abstract不能联合使用
三、抽象类与抽象方法的关系
1、抽象类中可以有或没有抽象方法或非抽象方法
2、抽象类中不一定有抽象方法,但是抽象方法必须出现在抽象类或接口中
3、一个非抽象的类继承抽象类,必须将抽象类中的抽象方法进行重写/覆盖,或者称为“对抽象的实现”
四、抽象类的子类也可以是抽象类
在 Java 中,抽象类的子类也可以是抽象类。这种继承关系可以一直延续下去,子类可以选择性地实现抽象方法,或者继续声明新的抽象方法。
具体来说,在 Java 中:
- 抽象类可以继续被继承,子类可以选择性地实现抽象方法,或者继续声明新的抽象方法。
- 抽象类的子类如果没有实现父类的所有抽象方法,那么子类本身也必须声明为抽象类。
以下是一个 Java 示例,说明了抽象类的子类也可以是抽象类:
abstract class AbstractShape {
abstract double area();
}
abstract class AbstractPolygon extends AbstractShape {
abstract int sides();
}
class Triangle extends AbstractPolygon {
private double base;
private double height;
Triangle(double base, double height) {
this.base = base;
this.height = height;
}
double area() {
return 0.5 * base * height;
}
int sides() {
return 3;
}
}
abstract class Quadrilateral extends AbstractPolygon {
abstract double diagonal();
}
class Rectangle extends Quadrilateral {
private double width;
private double height;
Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
double area() {
return width * height;
}
int sides() {
return 4;
}
double diagonal() {
return Math.sqrt(width * width + height * height);
}
}
public class Main {
public static void main(String[] args) {
Triangle triangle = new Triangle(4, 5);
Rectangle rectangle = new Rectangle(3, 6);
System.out.println("Triangle Area: " + triangle.area()); // 输出: Triangle Area: 10.0
System.out.println("Triangle Sides: " + triangle.sides()); // 输出: Triangle Sides: 3
System.out.println("Rectangle Area: " + rectangle.area()); // 输出: Rectangle Area: 18.0
System.out.println("Rectangle Sides: " + rectangle.sides()); // 输出: Rectangle Sides: 4
System.out.println("Rectangle Diagonal: " + rectangle.diagonal()); // 输出: Rectangle Diagonal: 6.708203932499369
}
}
在这个 Java 示例中,AbstractPolygon
是 AbstractShape
的子类,Quadrilateral
是 AbstractPolygon
的子类,Triangle
和 Rectangle
分别是 AbstractPolygon
和 Quadrilateral
的子类。这种继承关系形成了一个层次结构,子类可以选择性地实现父类的抽象方法,或者继续声明新的抽象方法。
package com.javase.抽象类;
public class AbstractTest01 {
public static void main(String[] args) {
//java: com.javase.抽象类.Account是抽象的; 无法实例化
//new Account();
}
}
//抽象类
//java: 非法的修饰符组合: abstract和final
/*final abstract class Account extends Object{
}*/
//抽象类
abstract class Account extends Object{
public Account(){
super();
}
//抽象方法
public abstract void doSome();
//非抽象方法
public void doOther(){
}
}
//子类继承抽象类,子类可以实例对象
/*class CreditAccount extends Account{
public CreditAccount(){
super();
}
}*/
//抽象类的子类也可以是抽象类
/*
abstract class CreditAccount extends Account{
}*/
五、一个非抽象的类继承抽象类,必须将抽象类中的所有抽象方法进行重写/覆盖
在 Java 中,一个非抽象的类继承抽象类时,必须实现抽象类中的所有抽象方法。这是因为抽象方法在抽象类中只有方法签名,没有具体的实现,而子类必须提供这些方法的具体实现。
如果一个非抽象的类继承了一个抽象类但没有实现抽象方法,那么这个子类本身也会被视为抽象类,无法被实例化。
以下是一个 Java 示例,演示了一个非抽象的类继承抽象类并实现抽象方法的情况:
abstract class AbstractShape {
abstract double area();
}
class Circle extends AbstractShape {
private double radius;
Circle(double radius) {
this.radius = radius;
}
double area() {
return 3.14159 * radius * radius;
}
}
class Square extends AbstractShape {
private double side;
Square(double side) {
this.side = side;
}
double area() {
return side * side;
}
}
public class Main {
public static void main(String[] args) {
Circle circle = new Circle(5);
Square square = new Square(4);
System.out.println("Circle Area: " + circle.area()); // 输出: Circle Area: 78.53975
System.out.println("Square Area: " + square.area()); // 输出: Square Area: 16.0
}
}
在这个 Java 示例中,Circle
和 Square
都是非抽象的子类,它们继承了 AbstractShape
并实现了 area
抽象方法,提供了具体的计算面积的实现。
如果 Circle
或 Square
没有提供 area
方法的实现,它们将被视为抽象类,无法实例化。这是确保抽象类的抽象方法得到了正确实现的一种机制。
package com.javase.抽象类;
public class AbstractTest02 {
public static void main(String[] args) {
//面向抽象编程,不要面向具体编程,降低程序的耦合度,提高程序的扩展力,符合OCP原则
Animal a = new Bird(); //向上转型
/*
* 编译时:
* 静态绑定为Animal中的move方法
* 运行时:
* 动态绑定为Bird中的move方法
* */
a.move();
Bird b = (Bird) a;
b.m1();
}
}
//抽象类
abstract class Animal extends Object{
//定义抽象方法
public abstract void move();
}
//非抽象子类
//java: com.javase.抽象类.Bird不是抽象的, 并且未覆盖com.javase.抽象类.Animal中的抽象方法move()
/*
class Bird extends Animal{
}*/
//非抽象子类
class Bird extends Animal{
//需要将父类中继承过来的抽象方法进行覆盖/重写,或者也可以叫做“实现”
public void move(){
System.out.println("鸟儿在飞翔!");
}
public void m1(){
System.out.println("m1");
}
}
//抽象子类:如果子类也是抽象的,则不需要覆盖,重写,或者实现父类中的抽象方法
/*
abstract class Bird extends Animal{
// public abstract void move();
}*/
第四章、接口(interface 完全抽象类)
一、接口概述
1、接口是完全抽象类
接口是完全抽象的。(抽象类是半抽象) 或者也可以说接口是特殊的抽象类
接口是一种定义了一组方法签名的抽象类型。它只描述了方法应该做什么,而不涉及具体的实现。接口定义了一种协议或契约,它规定了类必须实现的方法和行为。可以理解为:接口是特殊完全抽象的类,不是继承于Object
2、接口是用于数据类型
接口也是一种“引用数据类型”。编译之后也是一个class字节码文件
3、接口的意义
接口的主要目的是定义一组规范或契约,而不是提供具体的实现。通过实现接口,类可以遵循接口定义的规范并提供相应的方法实现。这种设计提供了一种分离接口和实现的方式,增加了代码的灵活性和可维护性。
接口就像是可插拔的内存条,通常提取的是行为动作
4、接口并不继承Object类
在Java中,接口(interface)并不直接继承自Object
类。接口是一种抽象类型,它定义了一组方法的签名,但没有实现这些方法的具体细节。由于接口不包含成员变量或方法的实现,因此它不需要继承自Object
类。
尽管接口本身不继承自Object
类,但实现接口的类是继承自Object
类的。当一个类实现了一个接口时,它默认继承了Object
类,并且可以使用Object
类中的方法。
无论类是否实现了接口,所有的类在Java中都会隐式地继承自Object
类。这意味着即使一个类没有显式地声明它继承自Object
类,它仍然会继承Object
类的方法和行为。
需要注意的是,接口中的方法都是隐式地被声明为抽象方法,而抽象方法本身是没有方法体的。因此,接口中的方法不同于Object
类中的方法,后者有默认的实现。
二、语法
[修饰符列表] interface 接口名{
常量;//默认为静态属性 public static final
抽象方法;//默认为实例方法 public abstract
}
三、接口支持多继承
接口支持多继承,一个接口可以继承(extends)多个接口
在Java中,接口是一种特殊类型的类,它只包含抽象方法的定义。与类不同,Java接口支持多继承,这意味着一个接口可以继承多个其他接口。这种多继承的概念称为"接口多继承"或"接口继承"。
当一个接口继承多个接口时,它会继承这些接口的所有抽象方法,并将它们合并到自己的接口定义中。这样,实现这个接口的类就需要提供实现所有继承的接口中的抽象方法。
下面是一个示例来说明接口的多继承:
// 定义接口A
interface A {
void methodA();
}
// 定义接口B
interface B {
void methodB();
}
// 定义接口C,继承接口A和接口B
interface C extends A, B {
void methodC();
}
// 实现接口C的类
class MyClass implements C {
public void methodA() {
System.out.println("实现接口A的方法");
}
public void methodB() {
System.out.println("实现接口B的方法");
}
public void methodC() {
System.out.println("实现接口C的方法");
}
}
// 主类
public class Main {
public static void main(String[] args) {
MyClass obj = new MyClass();
obj.methodA();
obj.methodB();
obj.methodC();
}
}
在上面的示例中,接口A
定义了方法methodA
,接口B
定义了方法methodB
,接口C
继承了接口A
和接口B
,并定义了方法methodC
。然后,类MyClass
实现了接口C
,并提供了对所有三个方法的实现。
在主类Main
中,我们创建了MyClass
的实例并调用了三个方法。由于MyClass
实现了接口C
,它必须提供对所有三个方法的实现。因此,输出将会是:
实现接口A的方法
实现接口B的方法
实现接口C的方法
这个例子展示了接口多继承的概念。通过继承多个接口,可以将多个接口的功能组合到一个接口中,并通过实现这个接口的类来提供实现。这样的设计可以帮助实现代码的灵活性和重用性。
四、接口中内容
1、接口中只包含两部分内容,一部分是:常量。一部分是:抽象方法。接口中没有其它内容(老版本的JDK中是这样的)
2、接口中所有的元素都是public修饰的。(都是公开的)。
3、接口中的抽象方法定义时:public abstract修饰符可以省略。
4、接口中的常量定义时: public static final可以省略。
5、接口中的方法都是抽象方法,所以接口中的方法不能有方法体
1、抽象方法(Abstract Methods)实例方法
接口中的方法没有具体的实现,只有方法的声明,没有方法体。它们通常没有方法体是因为接口用于定义一组规范,而具体的实现由实现接口的类提供。
1.1、接口中的抽象方法只能是public abstract 修饰的
接口中的抽象方法只能是public abstract修饰,可以省略
抽象类中的抽象方法除了不能使用private修饰外,缺省,protected,public 都可以搭配abstract修饰
2、常量(Constants)(实例变量/静态变量)
接口中可以包含常量,这些常量默认为public static final
,也就是说它们是公共的、静态的和不可修改的。在接口中定义的常量可以在实现类中直接访问。
3、嵌套接口(默认为public static)
在Java中,接口内部是可以嵌套其他接口的,也就是一个接口可以包含另一个接口
// 定义接口A
interface A {
void methodA();
// 嵌套接口B
//默认为public static
public static interface B {
void methodB();
}
}
// 实现接口A和接口A中的嵌套接口B
class MyClass implements A,A.B {
@Override
public void methodA() {
System.out.println("实现接口A的方法");
}
@Override
public void methodB() {
System.out.println("实现接口B的方法");
}
}
// 使用接口和实现类
public class Main {
public static void main(String[] args) {
MyClass obj = new MyClass();
//A类型不行,没有methodB方法,静态绑定失败,编译报错
// A obj = new MyClass();
//A.B类型不行,没有methodA方法,静态绑定失败,编译报错
// A.B obj = new MyClass();
obj.methodA(); // 调用接口A中的方法
obj.methodB(); // 调用接口B中的方法
}
}
在上面的示例中,接口A内部嵌套了接口B,并分别定义了各自的方法methodA
和methodB
。然后,MyClass
类实现了接口A和接口A中的嵌套接口B,并实现了这两个接口的方法。
请注意,对于嵌套接口B,您可以通过"接口A.接口B"的方式来引用它。在MyClass
中,我们实现了接口A和接口A中的嵌套接口B的方法,并在Main
类中进行了调用。
嵌套接口的使用可以帮助组织和结构化代码,尤其在一个接口中定义了多个相关联的子接口时,可以增强代码的可读性和可维护性。
4、新特性:默认方法(Default Methods)
在 Java 8 及之后的版本中,引入了接口的默认方法(Default Methods)的概念,允许接口中包含具有默认实现的方法。这使得在向现有接口添加新方法时,不会影响已经实现该接口的类,从而更好地支持接口的演化和扩展。也使得在接口的所有实现类中都有一个默认的方法实现,而不需要在每个实现类中都重复编写相同的代码。接口的默认方法在某种程度上类似于抽象类中的普通方法。
以下是关于 Java 接口默认方法的详细说明:
定义默认方法: 在接口中可以定义默认方法,使用 default
关键字标记。默认方法可以提供方法的默认实现,但它们可以在实现接口的类中被覆盖。
interface MyInterface {
// 默认方法
default void myDefaultMethod() {
System.out.println("This is a default method.");
}
// 抽象方法(普通接口方法)
void myAbstractMethod();
}
使用默认方法: 类实现接口时,默认方法会自动继承到实现类中。实现类可以选择性地覆盖默认方法,提供自己的实现。
class MyClass implements MyInterface {
@Override
public void myAbstractMethod() {
System.out.println("Implementing myAbstractMethod.");
}
}
public class Main {
public static void main(String[] args) {
MyClass obj = new MyClass();
obj.myDefaultMethod(); // 输出: This is a default method.
obj.myAbstractMethod(); // 输出: Implementing myAbstractMethod.
}
}
5、新特性:静态方法(Static Methods)
静态方法不会被子接口继承,也不会被实现接口的类继承。它们只能通过接口名调用。
在 Java 8 及之后的版本中,接口(interface)可以包含静态方法。静态方法是属于接口本身的方法,可以通过接口名调用,不需要创建接口的实例。静态方法可以用于提供通用的功能或工具函数,与接口的实例无关。
静态方法与抽象方法之间有一些重要的区别:
- 实例方法需要通过实例调用,而静态方法通过接口名调用。
- 静态方法不能访问实例成员,只能访问静态成员。
- 实例方法可以被子类覆盖,而静态方法不能。
interface MyInterface {
// 静态方法
static void myStaticMethod() {
System.out.println("This is a static method in MyInterface.");
}
// 抽象方法
void myAbstractMethod();
}
interface SubMyInterface extends MyInterface{
}
class MyClass implements SubMyInterface {
@Override
public void myAbstractMethod() {
System.out.println("Implementing myAbstractMethod.");
}
}
public class Main {
public static void main(String[] args) {
MyInterface.myStaticMethod(); // 输出: This is a static method in MyInterface.
//编译报错:只能使用静态方法所在接口名调用,子接口也不行
//SubMyInterface.myStaticMethod();
//编译报错:只能使用静态方法所在接口名调用
//Main.myStaticMethod();
MyClass obj = new MyClass();
obj.myAbstractMethod(); // 输出: Implementing myAbstractMethod.
//编译报错:只能使用静态方法所在接口名调用
//obj.myStaticMethod();
System.out.println(obj instanceof SubMyInterface); //true
System.out.println(obj instanceof MyInterface); //true
}
}
总而言之,接口是一种规范化的定义,包含了抽象方法、默认方法、静态方法和常量,用于定义类应该具备的行为和功能。
package com.javase.接口基础语法;
public class Test01 {
public static void main(String[] args) {
//访问接口中的常量
System.out.println(MyMath.PI);
//常量不能修改
//MyMath.PI = 46;
}
}
//定义接口:A
interface A{
}
//定义接口:B,支持继承
interface B extends A{
}
//接口支持多继承
interface C extends A,B{
}
interface MyMath{
//常量
public static final double PI = 3.1415926;
//可以省略:public static final
//接口中随便写一个变量,看着像变量,其实是省略了public static final 是常量
int I = 8;
//抽象方法
public abstract int sum(int a, int b);
//可以省略public 和 abstract
int sum1(int a, int b);
//编译报错:java: 接口抽象方法不能带有主体
/*void doSome(){
}*/
int sub(int a, int b);
}
五、接口和类之间的关系(implements 实现)
继承关键字:extends
实现关键字:implements
- 类和类之间叫做继承,只支持单继承
- 接口和接口之间叫做继承,支持多继承
- 类和接口之间叫做实现,非抽象的类可同时实现多个接口
理解为:可以将"实现"看做"继承”,但是实现并不完全是继承,只有部分继承(常量部分),实现implements有部分继承链,继承extends有全部继承链,实现的是接口,接口就像一个模具,(比如要用橡皮泥捏一个人,有很多模具,手的模具,头的模具等等,每一个模具就是一个接口,实例方法就像是手模具中每个关节的轮廓,橡皮泥自身也要按照轮廓来改变,就像是实现接口中抽象方法,默认方法就像是模具安置在橡皮泥上的,不会拿走,接口中的常量,就像是模具的logo,也会印在橡皮泥上,不会拿走,就像继承一样,接口中的静态方法就像按照模具捏的这个过程一样,捏完之后会将模具拿走,因此静态方法为接口特有的,只能用接口来调用,不会继承)将一些特定的实例方法(包括抽象方法和默认方法)强制打磨给指定的类,这样该类也会有这些特定的实例方法,具有可插拔性。
A类 实现 B接口:理解为A继承B,B的型覆盖至A
C类 继承 B类,B类 实现 A接口:理解为B继承A,A的型只能覆盖到B,不能覆盖到C
1、接口中的静态属性(常量)是隐式地被实现类继承的
接口中的静态属性(常量)被实现类隐式继承的设计是为了提供一种便捷的方式,让实现类可以访问接口中定义的常量,以及在不引入冗余代码的情况下共享常量值。这种设计考虑了接口作为契约和规范的角色,以及实现类应该能够访问接口的常量,而不需要额外的继承或实现。
以下是一些解释为什么接口中的静态属性被隐式继承的原因:
-
契约的一致性: 接口代表了一种契约,即实现类应该具有一定的行为和属性。在这个契约中,常量也是一种属性。为了确保实现类遵循接口的契约,接口中的常量应该对实现类是可见的。
-
代码重用和简洁性: 如果实现类需要访问接口中的常量,而不隐式继承,那么每个实现类都需要显式地重复定义这些常量。这会导致冗余代码,并降低了代码的可维护性。通过隐式继承,可以在不引入冗余代码的情况下实现常量的共享和复用。
-
接口的目的: 接口的一个主要目的是为实现类提供一组公共行为和属性,而这些属性(包括常量)对于实现类来说应该是一致的。通过允许实现类隐式继承接口中的常量,可以更好地达到这个目的。
-
不破坏向后兼容性: 将接口中的常量隐式继承到实现类中,不会引入向后不兼容的改变。这意味着,如果将新的常量添加到接口中,现有的实现类仍然可以继续正常工作,因为它们已经继承了接口中的所有常量。
总之,接口中的静态属性被隐式地继承到实现类中是为了支持契约的一致性、代码重用和简洁性,并且遵循了接口作为规范的设计目标。这种设计能够更好地满足实现类对接口常量的访问需求,同时保持代码的可维护性和向后兼容性。
package 接口;
public interface MyInterface {
public static final double PI = 3.1415926;
}
class Math implements MyInterface{
public static void main(String[] args) {
System.out.println(MyInterface.PI); //3.1415926
System.out.println(Math.PI); //3.1415926
Math math = new Math();
System.out.println(math.PI); //3.1415926
}
}
2、接口中的静态方法不是隐式地被实现类继承的
在 Java 中,接口中的静态方法不被实现类隐式继承的原因是为了避免多继承的复杂性和歧义。允许接口中的静态方法隐式继承到实现类可能会引发以下问题:
-
多继承冲突: 如果一个类实现了多个接口,而这些接口中都有相同的静态方法,那么在调用时就会产生冲突。这会让代码的语义变得模糊,不清楚应该调用哪个接口的静态方法。
-
灵活性和可理解性: 隐式继承会增加代码的复杂性,可能导致代码不够清晰和易于理解。在 Java 中,接口是一种定义契约和行为的方式,而静态方法通常提供一些通用的功能。这种分离可以让代码更加灵活和可理解。
-
不同接口的用途: 不同的接口可能具有不同的用途和功能,其中的静态方法也可能是不同的。在调用接口的静态方法时,需要明确指定调用的是哪个接口,以便程序员更好地理解代码的含义。
虽然接口中的静态方法不被实现类隐式继承,但实现类仍然可以直接调用接口中的静态方法,就像调用普通的静态方法一样。这种设计可以避免冲突和混淆,同时保持代码的清晰性和可维护性。
以下是一个示例,说明为什么接口中的静态方法不隐式地被实现类继承:
interface MyInterface {
static void myStaticMethod() {
System.out.println("This is a static method in MyInterface.");
}
}
class MyClass implements MyInterface {
// 实现类不会隐式继承接口中的静态方法
}
public class Main {
public static void main(String[] args) {
// 不能通过实现类名调用接口的静态方法
// MyClass.myStaticMethod(); // 编译错误
// 可以直接通过接口名调用接口的静态方法
MyInterface.myStaticMethod(); // 输出: This is a static method in MyInterface.
}
}
在这个示例中,MyClass
不会隐式继承 MyInterface
中的静态方法,但可以通过接口名直接调用这个静态方法。这种设计避免了多继承冲突和代码复杂性。
3、非抽象的类实现接口,须将接口中所有的抽象(实例)方法全部实现为实例方法(覆盖、重写)
接口中抽象方法的访问权限修饰符默认为public,在实现抽象方法时,访问权限不能更低,只能更高,所以必须采用public修饰
package com.javase.接口;
public class Test01 {
public static void main(String[] args) {
//MyMath1是抽象的; 无法实例化
//new MyMath1();
//使用多态语法
//父类型的引用指向子类型的对象
MyMath1 mm = new MyMath();
//调用接口里面的方法(面向接口编程)
int result1 = mm.sum(1,2);
System.out.println(result1);
int result2 = mm.sub(2,1);
System.out.println(result2);
MyMath m2;
MyMath1 m3;
}
}
//特殊的抽象类,完全抽象的,叫做接口
interface MyMath1 {
double PI = 3.1415926;
int sum(int a, int b);
int sub(int a, int b);
}
//编写一个类,该类为非抽象的类
//这样可以
/*abstract class MyMath implements MyMath1 {
}*/
//java: com.javase.接口.MyMath不是抽象的, 并且未覆盖com.javase.接口.MyMath1中的抽象方法sub(int,int)
/*class MyMath implements MyMath1 {
}*/
//修正
class MyMath implements MyMath1 {
//重写/覆盖/实现 接口中的方法 (通常叫做实现)
//正在尝试分配更低的访问权限; 以前为public
/*int sum(int a, int b) {
return a + b;
}*/
public int sum(int a, int b) {
return a + b;
}
public int sub(int a, int b) {
return a - b;
}
}
4、一个非抽象的类可以同时实现多个接口
java中类和类只支持单继承。实际上单继承是为了简单而出现的,现实世界中存在多继承,java中的接口弥补了单继承带来的缺陷。
5、接口通过类实现类似于多态中的向下转型
1、一个非抽象类同时实现了多个接口,接口之间无继承关系,向下转型时,编译通过,但是运行时可能出现: classCastException
2、之前有一个结论:无论向上转型还是向下转型,两种类型之间必须要有继承关系,没有继承关系编译器会报错。(这句话不适用在接口方面。)最终实际上和之前还是一样,需要加: instanceof运算符进行判断向下转型养成好习惯。转型之前先if+instanceof进行判断。
- 向上转型:自动类型转换
- 向下转型:强制类型转换
- 平行转型:理解接口类型转换就像平行转型一样
案例一:
public class Test01 {
public static void main(String[] args) {
//编译运行都没问题
//类似于平行转型
// A a = new C();
// B b = (B) a;
//编译没问题,运行有问题
//运行时异常:java.lang.ClassCastException
A a = new D();
B b = (B) a;
}
}
interface A { }
interface B { }
class C implements A, B { }
class D implements A { }
案例二:
package com.javase.接口;
public class Test02 {
public static void main(String[] args) {
A a = new D();
B b = new D();
b.m2();
C c = new D();
c.m3();
//向下转型,这个编译和运行都没问题
B b1 = (B) a;
b1.m2();
//向下转型,经过测试:接口和接口之间在进行强制类型转换的时候,没有继承关系,也可以强转
//但是一定注意:运行时可能出现 ClassCastException 异常
//编译没问题,运行有问题
M m = new E();
//K k = (K)m;
//instanceof运算符类型保护
if(m instanceof K){
K k = (K)m;
}
D d = (D) a;
d.m2();
}
}
interface K{
}
interface M{
}
class E implements M{
}
//======================================================
/*interface X{
}
interface Y{
}
interface Z extends X,Y{ //接口和接口之间支持多继承
}*/
interface A{
void m1();
}
interface B{
void m2();
}
interface C{
void m3();
}
//实现多个接口,其实就是类似于多继承
class D implements A,B,C{
public void m1(){
System.out.println("m1");
}
public void m2(){
System.out.println("m2");
}
public void m3(){
System.out.println("m3");
}
}
6、extends和implements可同时实现
1、extends关键字在前,implements关键字在后
2、class 类名 implements 接口名{ }
等同于
class 类名 extends Object implements 接口名 {}
package com.javase.接口;
public class Test03 {
public static void main(String[] args) {
// Cat c = new Cat();
//同一个接口被不同的类实现,会有不同的效果
Flyable fly = new Cat();
fly.fly();
Flyable fly_pig = new Pig();
fly_pig.fly();
Flyable f1 = new Fish();
f1.fly();
}
}
//动物父类
class Animal extends Object{
public Animal(){
super();
}
}
//可飞翔的接口
//能插拔的就是接口
//内存条插到主板上,可以更换
//接口通常提取的是行为动作
interface Flyable{
void fly();
}
//动物类子类:猫类
class Cat extends Animal implements Flyable{
public Cat(){
super();
}
public void fly(){
System.out.println("飞猫起飞!");
}
}
//蛇类,如果你不想让它飞,可以不实现Flyable接口
//没有实现这个接口表示没有翅膀,就不能飞
class Snake extends Animal{
}
//想飞就插翅膀这个接口
class Pig extends Animal implements Flyable{
public void fly(){
System.out.println("我是一只会飞的猪");
}
}
//鱼类
//默认都会继承于Object
class Fish extends Object implements Flyable{
public void fly(){
System.out.println("会飞的鱼");
}
}
六、接口在开发中的作用(顾客点菜案例)
解耦合
1、面向接口编程,可以降低程序的耦合度,提高程序的扩展力,符合OCP开发原则
2、接口的使用离不开多态机制(接口 + 多态 才可以达到降低耦合度)
3、接口可以解耦合:
- 任何一个接口都有调用者和实现者
- 接口可以将调用者和实现者解耦合
- 调用者面向接口调用
- 实现者面向接口编写实现
以后大项目的开发,一般都是将项目分离成一个模块一个模块的,模块和模块之间采用接口衔接,降低耦合度。
1、FoodMenu.java
package com.javase.接口;
public interface FoodMenu {
void xihongshijidan();
void yuxiangrousi();
}
2、ChinaCooker.java
package com.javase.接口;
//中国厨师
//厨师是接口的实现者
public class ChinaCooker implements FoodMenu{
public void xihongshijidan(){
System.out.println("中餐师傅做的西红柿鸡蛋,东北口味");
}
public void yuxiangrousi(){
System.out.println("中餐师傅做的鱼香肉丝,东北口味");
}
}
3、AmericaCooker.java
package com.javase.接口;
//西餐厨师
//厨师是接口的实现者
public class AmericaCooker implements FoodMenu {
public void xihongshijidan(){
System.out.println("西餐师傅做的西红柿鸡蛋");
}
public void yuxiangrousi(){
System.out.println("西餐师傅做的鱼香肉丝");
}
}
4、Customer.java
package com.javase.接口;
//顾客
public class Customer {
/*
* 顾客手里有一个菜单
* Customer has a FoodMenu
* 记住:以后凡是能够使用 has a 来描述的,统一以属性的方式存在
* */
//定义实例变量,实例属性,面向抽象(接口)编程,降低耦合度,提高扩展力
private FoodMenu foodmenu;
public FoodMenu getFoodmenu() {
return foodmenu;
}
public void setFoodmenu(FoodMenu foodmenu) {
this.foodmenu = foodmenu;
}
/*
以下这样写,就表示写死了,(焊接了,没有可插拔了)
ChinaCooker cc;
AmericaCooker ac;
*/
public Customer(){
}
public Customer(FoodMenu foodmenu){
this.foodmenu = foodmenu;
}
//点菜方法
public void order(){
this.getFoodmenu().xihongshijidan();
this.foodmenu.yuxiangrousi();
}
public static void main(String[] args){
//创建中餐厨师
FoodMenu cooker1 = new ChinaCooker();
//创建西餐厨师
FoodMenu cooker2 = new AmericaCooker();
//创建顾客1
Customer customer = new Customer(cooker1);
customer.order();
//创建顾客2
Customer customer1 = new Customer(cooker2);
customer1.order();
}
/*
* Cat is a Animal : 但凡满足 is a 的表示都可以设置为继承
* Customer has a FoodMenu :但凡是满足 has a 的表示都以属性的形式存在
* */
}
七、类型和类型之间的关系
is a (继承), has a(关联) , like a(实现)
1、is a(A是B,就是A继承于B)
- Cat is a Animal (猫是一个动物)
- 凡是能够满足 is a 的表示“继承关系”
A extends B
2、has a(A有B,就是B作为A类的实例变量)
- I has a pen(我有一只钢笔)
- 凡是能够满足 has a 关系的表示“关联关系”
- 关联关系通常是以“实例变量” 的形式存在
class A {
B b;
}
3、like a(A有B这样的能力,就是A实现了B接口)
- Cooker like a FoodMenu(厨师像一个菜单一样)
- 凡是能够满足 like a 关系的表示“实现关系”
- 实现关系通常是:类实现接口
class A implements B {
}
八、接口和抽象类之间的关系
- 抽象类是半抽象的,接口是完全抽象的。
- 抽象类中有构造方法,接口中没有构造方法
- 接口和接口之间支持多继承,类和类之间只能单继承。
- 一个非抽象类可同时实现多个接口,一个抽象类只能继承一个类(单继承)
- 接口中只允许出现常量和抽象方法。
- 以后接口使用的比抽象类多。一般抽象类使用的还是少,接口一般都是对“行为”的抽象。
第五章、Object类
一、概述
在Java中,Object
类是所有类的根类。它是Java类层次结构的顶级父类,因此每个Java类都直接或间接地继承自Object
类。Object
类位于java.lang
包中,所以它不需要显式导入。
二、常用方法
Object
类提供了一些通用的方法,这些方法可以被所有Java类继承和使用。Object
类中的这些方法部分是没有被声明为final
的,因此可以被子类重写。- 由于所有的类都继承自
Object
类,因此可以在任何对象上调用Object
类中的方法。这使得在进行一些通用操作时,比如对象的比较、转换为字符串等,非常方便。
三、toString( )
输出实例引用的时候,会自动调用toString方法
println()
方法需要将对象转换为字符串,然后将字符串打印到控制台。而Object
类中的toString()
方法提供了一个通用的方式来获取对象的字符串表示形式。因此,为了方便输出对象引用,Java在println()
方法内部自动调用对象的toString()
方法。
1、默认实现
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
源代码上的toString方法的默认实现是:
返回 完整类名 @ 对象的内存地址转换为十六进制的形式
这个默认实现返回的字符串由类名和对象的哈希码组成。例如,对于一个名为ObjectTest01的类的实例,toString()
方法的默认实现可能返回类似于常用类.Object类.ObjectTest01@29a的字符串。
2、设计目的
toString方法的设计目的是:通过调用这个方法可以将一个“java对象”转换成为“字符串表示形式”
3、重写方法
大多数类都会覆盖Object
类中的toString()
方法,以提供更有用的信息来描述对象的状态。这样做可以增加代码的可读性和调试的便利性。
重写原则:
- 越简洁越好,可读性越强越好
- 向简洁的,详实的,易阅读的方向发展
package com.javase.Object类方法;
public class Test01 {
public static void main(String[] args) {
Person p = new Person();
//未重写toString方法
//System.out.println(p.toString()); //com.javase.Object类方法.Person@4554617c
//重写后的toString方法
System.out.println(p.toString()); //Person{name=null, age=0}
System.out.println(p);
}
}
class Person {
private String name;
private int age;
// 构造方法和其他代码省略
@Override
public String toString() {
return "Person{name=" + name + ", age=" + age + "}";
}
}
通过覆盖
toString()
方法,我们可以根据对象的特定属性提供更有用的信息,并根据需要自定义输出格式。
4、System.out.println输入对象引用时自动调用toString方法原理
四、equals( )
Object
类中的equals(Object obj)
方法用于比较当前对象与指定对象是否相等。默认情况下,equals()
方法使用的是对象的引用比较,即只有当两个对象引用同一个内存地址时才被认为是相等的(返回true)。
1、默认实现
public boolean equals(Object obj) {
return (this == obj);
}
这个默认实现使用的是对象的引用比较,即比较当前对象和指定对象的引用是否相同。如果两个对象引用相同的内存地址,equals()
方法返回true
,否则返回false
。
2、设计目的
equals方法是判断两个对象是否相等的
3、重写方法
判断两个实例对象是否相等,不能使用“==”,因为“==”比较的是两个实例对象的内存地址,这样的话,即使两个对象初始化相同,但是因为内存地址不同,也会返回false,
- equals方法默认实现就是采用“==”来比较,而“==”判断的是两个实例对象的内存地址,我们应该判断两个实例对象的内容是否相等,所以要进行方法重写
- String类已经重写了equals方法和toString方法
重写原则:
-
自反性(Reflexive):对于任意非空对象
x
,x.equals(x)
应该返回true
。 -
对称性(Symmetric):对于任意非空对象
x
和y
,如果x.equals(y)
返回true
,则y.equals(x)
也应该返回true
。 -
传递性(Transitive):对于任意非空对象
x
、y
和z
,如果x.equals(y)
返回true
,并且y.equals(z)
也返回true
,则x.equals(z)
应该返回true
。 -
一致性(Consistent):对于任意非空对象
x
和y
,多次调用x.equals(y)
应该始终返回相同的结果,前提是在比较过程中对象的状态没有发生变化。 -
对于任意非空对象
x
,x.equals(null)
应该返回false
。
4、重写方法--实例属性为基本类型情况(使用 == )
package com.javase.Object类方法;
public class Test03 {
public static void main(String[] args) {
MyTime t1 = new MyTime(2008,8,8);
MyTime t2 = new MyTime(2008,8,8);
System.out.println(t1 == t2); //false
//重写equals方法之前(比较的是实例对象的内存地址)
//System.out.println(t1.equals(t2)); //false
//重写equals方法之后(比较的是实例对象的内容)
System.out.println(t1.equals(t2)); //true
//在创建一个对象
MyTime t3 = new MyTime(2008,8,9);
System.out.println(t1.equals(t3)); //false
MyTime t4 = null;
System.out.println(t1.equals(t4)); //false
}
}
class MyTime {
int year;
int month;
int day;
public MyTime(){
}
public MyTime(int year,int month,int day){
this.year = year;
this.month = month;
this.day = day;
}
/*@Override
public boolean equals(Object obj){
//当 年 相同,月 相同,日 相同,表示两个日期相同,两个对象相等
//获取第一个日期的年月日
int year1 = this.year;
int month1 = this.month;
int dai1 = this.day;
//获取第二个日期的年月日
//类型保护,向下转型
if(obj instanceof MyTime){
MyTime t2 = (MyTime) obj;
int year2 = t2.year;
int month2 = t2.month;
int day2 = t2.day;
if(year1 == year2 && month1 == month2 && dai1 == day2){
return true;
}
}
//程序执行到此处,会返回false
return false;
}*/
/*@Override
public boolean equals(Object obj){
if(obj == null){
return false;
}
if(!(obj instanceof MyTime)){
return false;
}
//如果内存地址相同的时候,指向堆内存的对象肯定是同一个
if(this == obj){
return true;
}
//程序能执行到此处,说明obj一定是MyTime的实例
MyTime t = (MyTime) obj;
*//*if(this.year == t.year && this.month == t.month && this.day == t.day){
return true;
}
//程序执行到此处,会返回false
return false;*/
/*
return this.year == t.year && this.month == t.month && this.day == t.day;
}*/
@Override
public boolean equals(Object obj){
if(obj == null || !(obj instanceof MyTime)){
return false;
}
//如果内存地址相同的时候,指向堆内存的对象肯定是同一个
if(this == obj){
return true;
}
//程序能执行到此处,说明obj一定是MyTime的实例
MyTime t = (MyTime) obj;
return this.year == t.year && this.month == t.month && this.day == t.day;
}
}
5、重写方法--实例属性为引用类型情况(使用 equals方法 )
package com.javase.Object类方法;
public class Test05 {
public static void main(String[] args) {
Student s1 = new Student(1,"希望小学");
Student s2 = new Student(1,"希望小学");
System.out.println(s1 == s2); //false
System.out.println(s1.equals(s2)); //true
Student s3 = new Student(1,new String("希望小学"));
Student s4 = new Student(1,new String("希望小学"));
System.out.println(s3 == s4); //false
System.out.println(s3.equals(s4)); //true
}
}
class Student{
//学号
private int id;
private String school;
public Student(){
}
public Student(int id,String school){
this.id = id;
this.school = school;
}
//重写toString方法
@Override
public String toString(){
return "学号:" + id + ",学校:" + school;
}
//重写equals方法
//需求:当一个学生的学号相等,并且学校也相同时,表示同一个学生
@Override
public boolean equals(Object obj){
if(this == obj){
return true;
}
if(obj == null || !(obj instanceof Student)) return false;
Student s = (Student) obj;
return this.id == s.id && this.school.equals(s.school);
}
}
6、“==”和equals方法的使用区别
- java中基本数据类型比较是否相等,使用“==”
- “==” 比较的始终是变量或引用中保存的值
- java中引用数据类型比较是否相等,使用equals方法
- equals()方法相当于是在 “==” 的基础上进行了进一步的封装,可以比较其他内容
7、equals方法重写要彻底
Java中引用数据类型比较内容时,必须使用equals方法,当在自定义的类中使用equals方法进行比较时,一定要确保自定义类中的equals方法已经重写
package com.javase.Object类方法;
public class Test06 {
public static void main(String[] args) {
User1 u1 = new User1("zhangsan",new Address("西安","长延堡街道","710000"));
User1 u2 = new User1("zhangsan",new Address("西安","长延堡街道","710000"));
System.out.println(u1.equals(u2)); //true
User1 u3 = new User1("lisi",new Address("西安","长延堡街道","710000"));
System.out.println(u1.equals(u3)); //false
}
}
class Address{
private String city;
private String street;
private String zipcode;
public Address(){}
public Address(String city,String street,String zipcode){
this.city = city;
this.street = street;
this.zipcode = zipcode;
}
//重写equals方法
//重写规则:城市,街道,和邮编有相同时,则对象相等
public boolean equals(Object obj){
if(this == obj) return true;
if(obj == null || !(obj instanceof Address)) return false;
Address address = (Address) obj;
return this.city.equals(address.city)
&& this.street.equals(address.street)
&& this.zipcode.equals(address.zipcode);
}
}
class User1{
private String name;
private Address address;
public User1() {
}
public User1(String name, Address address) {
this.name = name;
this.address = address;
}
//重写equals方法
//重写规则:当一个用户的用户名和家庭住址都相同,则表示为同一个用户
public boolean equals(Object obj){
if(this == obj) return true;
if(obj == null || !(obj instanceof User1)) return false;
User1 user1 = (User1) obj;
return this.name.equals(user1.name) && this.address.equals(user1.address);
}
}
五、finalize( )
在Java中,Object
类中定义了一个finalize()
方法。finalize()
方法是一个被称为“终结器”(finalizer)的特殊方法。它在对象被垃圾回收器回收之前被调用,用于进行一些清理和释放资源的操作。
1、默认实现
protected void finalize() throws Throwable {
// 清理和释放资源的代码
}
finalize()
方法是一个受保护的方法,子类可以选择性地覆盖它来执行自定义的清理操作。当对象变得不可达时(即没有引用指向它),垃圾回收器会在对象被回收之前调用finalize()
方法。
需要注意的是,尽管finalize()
方法可以被覆盖,但在现代的Java编程中,它已经不推荐使用。这是因为finalize()
方法的行为是不确定的,不能保证它会被及时地执行或执行多少次。此外,finalize()
方法的调用会导致性能损失,并可能导致一些潜在的问题,例如资源泄漏。因此,Java 9中已经将finalize()
方法标记为@Deprecated
,并计划在将来的版本中将其删除。
相比于使用finalize()
方法,更好的做法是使用try-finally
块或使用AutoCloseable
接口来确保资源的释放和清理。通过这种方式,可以更可靠地管理对象的生命周期和资源的释放,而不依赖于垃圾回收器的不确定行为。
总结来说,Object
类中的finalize()
方法是一个用于在对象被垃圾回收之前执行清理操作的特殊方法,但它已经不推荐使用。在现代的Java编程中,应使用其他方式来确保资源的释放和清理。
2、使用说明
- 1、GC垃圾回收机制:负责调用finalize()方法
- 2、finalize()方法只有一个方法体,里面没有代码,而且这个方法是protected修饰的。
- 3、这个方法不需要程序员手动调用,
- JVM的垃圾回收器负责调用这个方法不像equals tostring,equals和tostring)方法是需要你写代码调用的。finalize()只需要重写,重写完将来自动会有程序来调用。
- 4、finalize()方法的执行时机:
- 当一个java对象即将被垃圾回收器回收的时候,垃圾回收器负责调用finalize0)方法
- 5、finalize()方法实际上是SUN公司为iava程序员准备的一个时机
- 垃圾销毁时机。如果希望在对象销毁时机执行一段代码的话,这段代码要写到finalize()方法当中
- 6、执行时机
- static 静态代码块在类加载时刻执行,并且只执行一次,这是一个SUN准备的类加载时机
- finalize() 方法同样也是SUN为程序员准备的一个时机。这个时机是垃圾回收时机。
- 7、提示:java中的垃圾回收器不是轻易启动的,垃圾太少,或者时间没到,种种条件下有可能启动,也有可不启动.
package com.javase.Object类方法;
public class Test07 {
public static void main(String[] args) {
/*//创建对象
Person1 p = new Person1();
//销毁对象
p = null;*/
//多造点垃圾
/*for(int i = 1; i < 10000000; i++){
Person1 p = new Person1();
p = null;
}*/
//建议启动垃圾回收器
for(int i = 1; i < 1000; i++){
Person1 p = new Person1();
p = null;
//建议启动垃圾回收器,(只是建议,有可能启动,也有可能不启动)
System.gc();
}
}
}
//项目开发中有这样的业务需求,所有对象在JVM中被释放的时候,请记录一下释放时间!!
//记录对象被释放的时间点,这个负责记录时间的代码写在 finalize方法中
class Person1 extends Object{
protected void finalize() throws Throwable {
System.out.println(this + "即将被销毁!");
}
}
六、hashCode( )
需要注意的是,如果你重写了hashCode()方法,也应该同时重写equals() 方法,以确保哈希码和相等性的一致性。
在Java中,hashCode()
方法是Object
类定义的一个方法,用于计算对象的哈希码(hash code)。哈希码是一个整数值,用于快速确定对象是否相等或用于在哈希表等数据结构中进行快速查找。
hashCode()
方法的计算方式可以根据对象的内容或其内部的一些属性而有所不同。然而,hashCode()
方法必须满足以下两个条件:
- 如果两个对象通过
equals()
方法判断相等(obj1.equals(obj2)
返回true
),那么它们的哈希码必须相等,即obj1.hashCode() == obj2.hashCode()
。 - 对于不相等的对象,哈希码不需要保证不同,即两个对象的哈希码相等并不意味着它们相等。
通常情况下,为了提高哈希码的散列性和分布性,应该选择一种合适的算法来计算哈希码。在Java中,常用的计算哈希码的方式是将对象的各个属性进行运算和组合。
在实际编程中,如果自定义的类需要使用哈希码进行对象比较或在集合中使用,通常需要重写hashCode()
方法。重写hashCode()
方法时,应遵循以下原则:
- 如果
equals()
方法判断两个对象相等,那么它们的哈希码必须相等。 - 在计算哈希码时,应该使用对象的关键属性,这些属性在对象相等时应该保持一致。
- 哈希码的计算应该尽量均匀地分布在整个哈希码空间中,以避免哈希冲突,提高哈希表等数据结构的性能。
为了方便重写hashCode()
方法,Java提供了Objects
工具类中的hashCode(Object...)
方法,用于计算多个属性的哈希码。这个方法会自动处理null
值和基本类型的转换。
总结来说,hashCode()
方法是用于计算对象哈希码的方法。重写hashCode()
方法时需要遵循一定的原则,以保证相等的对象具有相等的哈希码,并尽量提高哈希码的散列性和分布性。
package com.javase.Object类方法;
public class Test08 {
public static void main(String[] args) {
Object obj = new Object();
int hashCodeValue = obj.hashCode();
//对象内存地址经过哈希算法转换的一个数字,可以等同看做内存地址。
System.out.println(hashCodeValue); //2129789493
}
}
七、关于Object类中的wait和notify方法。(详见多线程)
1、概述
wait和notify方法不是线程对象的方法,是java中任何一个java对象都有的方法,因为这两个方法是Object类中自带的。
wait方法和notify方法不是通过线程对象调用
不是这样的:t.wait()也不是t.notify()。。。不对
2、wait()方法的作用
Object o = new Obejct();
o.wait();
wait()方法会让当前线程释放掉锁,进入锁池,相当于该线程终止了,不会执行wait后续的代码
表示:
让正在o对象上活动的线程进入等待状态,无限期等待,直到被唤醒为止
o.wait()方法的调用,会让“当前线程(正在o对象上活动的线程)”进入等待状态
3、notify()方法的作用
Object o = new Object();
o.notify();
表示:
唤醒正在o对象上等待的线程
还有一个notifyAll()方法:表示唤醒o对象上处于等待的所有线程
4、wait和notify方法的理解
在 Java 中,Object 类提供了三个用于线程同步的方法:wait()、notify() 和 notifyAll()。
-
wait():
- wait() 方法使当前线程进入等待状态,释放对象的锁,并等待其他线程通过 notify() 或 notifyAll() 方法来唤醒它。
- wait() 方法必须在同步块或同步方法内部调用,否则会抛出 IllegalMonitorStateException 异常。
- 调用 wait() 后,线程会进入等待池中,直到被其他线程通过 notify() 或 notifyAll() 唤醒,或者等待时间到达。
-
notify():
- notify() 方法用于唤醒等待池中的一个线程,并使其进入抢夺锁状态。
- notify() 方法必须在同步块或同步方法内部调用,否则会抛出 IllegalMonitorStateException 异常。
- 如果有多个线程在等待同一个对象的锁,调用 notify() 方法会随机唤醒其中一个线程。
-
notifyAll():
- notifyAll() 方法用于唤醒等待池中的所有线程,使它们进入抢夺锁状态。
- notifyAll() 方法必须在同步块或同步方法内部调用,否则会抛出 IllegalMonitorStateException 异常。
区别与联系:
- wait()、notify() 和 notifyAll() 方法都必须在同步块或同步方法内部使用,因为它们依赖于对象的监视器锁(即 synchronized 关键字)。
- wait() 方法会释放当前线程持有的对象锁,并进入等待状态,直到被其他线程通过 notify() 或 notifyAll() 唤醒。notify() 方法会唤醒等待池中的一个线程,而 notifyAll() 方法会唤醒等待池中的所有线程。
- 线程调用 wait() 后会释放对象锁,进入等待池中,其他线程可以获取该锁。唤醒等待的线程后,它会重新尝试获取锁,从 wait() 方法返回后继续执行。
- 这些方法主要用于线程之间的通信和协调。它们通常与 synchronized 关键字一起使用,实现线程间的互斥和同步。
需要注意的是,使用 wait()、notify() 和 notifyAll() 方法时需要小心,确保在正确的地方调用,并避免潜在的死锁和竞争条件问题。正确使用这些方法可以实现线程之间的有效通信和同步。
八、生产者和消费者模式
1、概述
2、案例一
package com.javase.Object类方法.wait和notify方法;
import java.util.ArrayList;
import java.util.List;
/**
* 1、使用wait和notify方法实现“生产者和消费者模式”
* <p>
* 2、什么是“生产者和消费者模式”
* 生产线程负责生产,消费线程负责消费
* 生产线程和消费线程要达到均衡
* 这是一种特殊的业务需求,在这种特殊的情况下使用wait方法和notify方法
* <p>
* 3、wait和notify方法不是线程对象特有的方法,而是普通java对象都有的方法
* <p>
* 4、wait方法和notify方法建立在线程同步synchronized的基础之上,因为多线要同时操作一个仓库,有线程安全问题
* <p>
* 5、wait方法作用:o.wait()让正在o对象上活动的线程t进入等待状态,并且释放t线程之前占有的o对象的锁
* <p>
* 6、notify方法作用:o.notify()让正在o对象上等待的线程唤醒,只是通知,并不会释放该线程在o对象上之前占的锁
* <p>
* 7、模拟这样一个需求:
* 仓库我们采用List集合
* List集合中假设只能存储一个元素
* 1个元素就表示仓库满了
* 如果List集合中元素个数为0,就表示仓库空了
* 保证List集合中永远都是最多存储1个元素
* <p>
* 必须做到这种效果:生产1个消费1个
*/
public class ThreadTest01 {
public static void main(String[] args) {
//创建一个仓库对象(共享的)
List list = new ArrayList();
//创建两个线程对象
//生产者线程
Thread t1 = new Thread(new Producer(list));
//消费者线程
Thread t2 = new Thread(new Consumer(list));
t1.setName("生产者线程");
t2.setName("消费者线程");
//启动线程
t1.start();
t2.start();
}
}
/**
* 必然是生产者线程先执行
*/
//生产线程类
class Producer implements Runnable {
//仓库
private List list;
public Producer(List list) {
this.list = list;
}
@Override
public void run() {
int i = 0;
//一直生产,死循环
while (i < 10) {
//给仓库对象list加锁
synchronized (list) {
if (list.size() > 0) { //大于0,说明仓库中已经有1个元素了
//当前线程进入等待状态,并且释放Producer对象之前占有List集合对象的锁
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//程序能到这里,说明仓库是空的,可以生产
Object obj = new Object();
list.add(obj);
System.out.println(Thread.currentThread().getName() + "-->" + obj);
//唤醒消费者进行消费
list.notify();
}
i++;
}
}
}
//消费线程类
class Consumer implements Runnable {
//仓库
private List list;
public Consumer(List list) {
this.list = list;
}
@Override
public void run() {
int i = 0;
//一直消费
while (i < 10) {
synchronized (list) {
//仓库已经空了
//消费者线程等待,释放list仓库对象的锁
if (list.size() == 0) {
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//程序能执行到此处,说明仓库中有数据,可以进行消费
Object obj = list.remove(0);
System.out.println(Thread.currentThread().getName() + "-->" + obj);
//唤醒生产者进行生产
// list.notify();
list.notifyAll();
}
i++;
}
}
}
3、案例二
package com.javase.Object类方法.wait和notify方法.测试;
class Message {
private int content;
private boolean isReady = false;
int flag = 1;
public synchronized void setContent(int content) {
while (isReady) {
try {
wait(); // 当消息已经准备好时,等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.content = content;
isReady = true;
notifyAll(); // 唤醒等待的线程
System.out.println("Produced: " + (flag));
}
public synchronized int getContent() {
while (!isReady) {
try {
this.wait(); // 当消息还未准备好时,等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
isReady = false;
this.notifyAll(); // 唤醒等待的线程
this.flag = this.content + 1 + 1;
return this.content;
}
}
//生产者线程类
class Producer implements Runnable {
private final Message message;
public Producer(Message message) {
this.message = message;
}
@Override
public void run() {
while (true) {
message.setContent(message.flag);
try {
Thread.sleep(1000); // 模拟生产过程的延迟
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Consumer implements Runnable {
private final Message message;
public Consumer(Message message) {
this.message = message;
}
@Override
public void run() {
while (true) {
int msg = message.getContent();
System.out.println("Consumed: " + (msg + 1));
try {
Thread.sleep(1000); // 模拟消费过程的延迟
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Main {
public static void main(String[] args) {
Message message = new Message();
Thread producerThread = new Thread(new Producer(message));
Thread consumerThread = new Thread(new Consumer(message));
producerThread.start();
consumerThread.start();
}
}
在这个例子中,有一个共享的Message对象,生产者线程通过setContent()方法设置消息内容,消费者线程通过getContent()方法获取消息内容。生产者和消费者之间使用wait()和notifyAll()进行通信和同步,确保在消息准备好时消费者可以获取到消息,并在消息被消费后生产者可以继续生产新的消息。
请注意,这个例子仅用于演示wait()、notify()和notifyAll()的基本用法和概念,并不代表在实际开发中的最佳实践。实际使用时,需要更加细致地处理线程的同步和互斥,并避免潜在的死锁和竞争条件问题。
第六章、System类
java.lang.System
类是 Java 标准库中的一个工具类,提供了与系统相关的属性和方法
一、常用属性
1、in
:标准输入流,类型为 InputStream
2、out
:标准输出流,类型为 PrintStream
3、err
:标准错误流,类型为 PrintStream
二、常用方法
1、System.currentTimeMillis()
:返回当前时间的毫秒数,类型为 long
获取自1970年1月1日 00:00:00 000到当前系统时间的总毫秒数 1秒 = 1000毫秒
// 获取当前时间的毫秒数
long currentTime = System.currentTimeMillis();
System.out.println("当前时间的毫秒数:" + currentTime); //1692580174803
2、System.exit(int status)
:终止当前正在运行的 Java 虚拟机 一般为0
参数
status
是退出状态码
在 Java 中,System.exit(int status)
方法用于终止 Java 虚拟机(JVM)的运行,并传递一个整数状态码作为退出代码。退出代码可以用于表示程序的执行状态,通常约定了一些常用的退出状态码,但并不限于这些。
通常情况下,退出状态码的约定如下:
- 返回 0:表示程序正常退出。
- 返回非零值:表示程序以错误状态退出,具体的非零值通常用于区分不同的错误类型。
例如,常见的状态码约定如下:
- 返回 1:通常用于表示通用的错误状态。
- 返回 2:通常用于表示命令行参数错误。
- 返回其他非零值:根据具体需要来自定义其他错误状态。
你可以根据你的程序逻辑和需求,自定义退出状态码以及对应的含义。以下是一个示例:
package 常用类.System类;
public class ExitExample {
public static void main(String[] args) {
int exitCode = processInput(args);
System.exit(exitCode);
}
private static int processInput(String[] args) {
// 处理命令行参数
if (args.length == 0) {
System.err.println("Missing input arguments.");
return 2; // 命令行参数错误
}
// 执行其他逻辑
// ...
if (args.length >= 10) {
System.err.println("An error occurred.");
return 1; // 通用错误状态
}
return 0; // 正常退出
}
}
总之,System.exit(int status)
方法可以传递任何整数状态码,但常见的约定是返回 0 表示正常退出,返回非零值表示错误状态,具体的非零值可以根据需要自定义。
3、System.gc()
:运行垃圾回收器
System.gc()
是 Java 中的一个方法,用于显式地请求进行垃圾回收(Garbage Collection)。垃圾回收是 Java 虚拟机(JVM)的一个重要特性,它负责自动释放不再被程序引用的内存资源,以避免内存泄漏和提高程序的性能。
尽管 Java 的垃圾回收通常由 JVM 自动管理,但在某些情况下,你可能希望手动触发垃圾回收以尽早释放不再使用的内存。这时可以使用 System.gc()
方法。
需要注意的是,调用 System.gc()
方法并不一定会立即触发垃圾回收,因为 JVM 会根据内部策略来判断是否需要进行垃圾回收。调用该方法只是向 JVM 发出一个建议,但 JVM 可能会忽略它。在大多数情况下,推荐依赖 JVM 的自动垃圾回收机制来处理内存管理。
以下是关于 System.gc()
方法的一些要点:
-
方法签名:
public static void gc()
-
作用: 显式地触发垃圾回收。
-
用途: 在特定情况下,可能需要强制执行垃圾回收,如在测试性能或内存泄漏时。然而,正常情况下很少需要手动触发垃圾回收。
package 常用类.System类;
public class Test extends Object{
public static void main(String[] args) {
System.out.println(System.currentTimeMillis());
for (int i = 0; i < 100; i++) {
Test t = new Test();
t = null;
System.gc();
}
}
@Override
protected void finalize() throws Throwable {
System.out.println("对象被销毁");
}
}
4、arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
:将一个数组的部分元素复制到另一个数组中
// 复制数组
int[] sourceArray = {1, 2, 3, 4, 5};
int[] destArray = new int[5];
System.arraycopy(sourceArray, 0, destArray, 0, sourceArray.length);
System.out.println("复制后的数组:" + Arrays.toString(destArray));
5、getProperty(String key)
:获取指定键的系统属性值,参数 key
是系统属性的键名。
// 获取系统属性值
String javaHome = System.getProperty("java.home");
System.out.println("Java 安装路径:" + javaHome);
第七章、内部类
一、概述
在Java中,内部类是定义在其他类内部的类。它们允许在一个类的内部创建另一个类,并且可以访问外部类的成员,包括私有成员。内部类提供了一种封装和组织代码的方式,可以实现更好的代码结构和逻辑关联。
内部类的特点和优势包括:
- 内部类可以访问外部类的私有成员,包括私有方法和私有字段。
- 内部类提供了更好的封装性和代码组织性,可以将相关的类和接口组织在一起。
- 内部类可以实现多重继承,一个类可以实现多个接口或继承一个类的同时实现一个接口。
- 内部类可以方便地访问外部类的实例,并且可以使用外部类的引用来访问外部类的成员。
内部类在Java中是一种强大的特性,它提供了更灵活的编程方式和更好的代码结构。然而,需要根据实际情况来选择是否使用内部类,以及选择适合的内部类类型。
Java中有四种类型的内部类:
二、实例内部类(Member Inner Class)
类似于实例方法,可用访问权限修饰符修饰
它是定义在类内部的普通类。成员内部类可以访问外部类的所有成员(包括私有成员),并且可以使用外部类的引用来访问外部类的实例。实例内部类的创建必须依赖于外部类的实例。
在 Java 中,实例内部类是定义在另一个类的内部的类,它与外部类的实例有关联,并且可以访问外部类的实例变量和方法。实例内部类可以用于封装和组织代码,同时允许访问外部类的成员,包括私有成员。
以下是实例内部类的一些特点和示例说明:
1、特点
- 实例内部类中可以有实例变量,实例方法,构造方法,静态变量,静态方法
- 实例内部类可以访问外部类的所有成员,包括私有成员。
- 实例内部类需要通过外部类的实例来创建和使用。
2、示例
假设我们有一个外部类 OuterClass
,并在其中定义一个实例内部类 InnerClass
:
package 内部类;
public class Test01 {
public static void main(String[] args) {
OuterClass outerObj = new OuterClass();
OuterClass.InnerClass innerObj = outerObj.new InnerClass();
innerObj.instaceInnerMethod();
/**
* 实例内部方法
* ================================
* 实例外部变量
* 实例内部变量
* 实例外部方法
* 静态外部变量
* 静态内部变量
* 静态外部方法
* 静态内部方法
* ================================
* 静态外部变量
* 静态内部变量
* 静态外部方法
*/
}
}
class OuterClass {
//实例外部变量
private String instanceOuterVar = "实例外部变量";
//静态外部变量
private static String staticOuterVar = "静态外部变量";
//实例外部方法
public void instanceOuterMethod() {
System.out.println("实例外部方法");
}
//静态外部方法
private static void staticOuterMethod(){
System.out.println("静态外部方法");
}
//实例内部类
public class InnerClass {
//实例内部变量
private String instanceInnerVar = "实例内部变量";
//静态内部变量
private static String staticInnerVar = "静态内部变量";
//实例内部方法
public void instaceInnerMethod(){
System.out.println("实例内部方法");
System.out.println("================================");
System.out.println(instanceOuterVar); //不能加this,this指代是内部类的实例对象
System.out.println(this.instanceInnerVar);
instanceOuterMethod(); //不能加this,this指代是内部类的实例对象
// this.instaceInnerMethod();
System.out.println(OuterClass.staticOuterVar);
System.out.println(InnerClass.staticInnerVar);
OuterClass.staticOuterMethod();
InnerClass.staticInnerMethod();
}
public static void staticInnerMethod(){
System.out.println("静态内部方法");
System.out.println("================================");
System.out.println(OuterClass.staticOuterVar);
System.out.println(InnerClass.staticInnerVar);
OuterClass.staticOuterMethod();
// InnerClass.staticInnerMethod();
}
}
}
总之,实例内部类允许在外部类的内部定义一个关联的嵌套类,并且可以方便地访问外部类的成员。但需要注意,在某些情况下,使用静态内部类可能更合适,因为它不依赖于外部类的实例。
三、静态内部类(Static Inner Class)
类似于静态方法,可用访问权限修饰符修饰
它是定义在类内部的静态类。静态内部类与外部类实例无关,可以直接通过外部类名访问。静态内部类是定义在另一个类内部的类,但与外部类的实例无关,它与外部类的实例独立存在。
1、定义静态内部类
静态内部类使用static关键字来修饰,这意味着该内部类可以在没有外部类对象的情况下被单独实例化。静态内部类的定义方式如下:
public class OuterClass {
// 外部类的成员和方法
public static class StaticInnerClass {
// 静态内部类的成员和方法
}
}
2、访问权限
静态内部类可以具有public、protected、默认(package-private)和private的访问权限,与普通类的访问权限规则相同。
3、静态内部类的特性
3.1、静态内部类不依赖于外部类的实例,可以在没有外部类对象的情况下直接实例化
public class OuterClass {
public static class StaticInnerClass {
// ...
}
}
// 实例化静态内部类
OuterClass.StaticInnerClass innerObj = new OuterClass.StaticInnerClass();
在上述示例中,可以直接实例化静态内部类 StaticInnerClass
,而不需要先创建外部类的实例。
3.2、静态内部类可以通过(外部类.静态变量/方法)访问外部类的静态成员变量和静态方法,但不能直接访问外部类的实例变量和实例方法,需要通过外部类的实例来访问(包括private属性和方法)
public class OuterClass {
//静态变量
private static int a;
//静态方法
private static void a1(){}
//实例变量
private int b;
//实例方法
private void b1(){}
public static class StaticInnerClass {
public void accessOuterClass() {
/**
* 在静态内部类中通过 外部类.静态变量/方法 访问
* 外部类名可省略,直接访问外部类中的静态成员
*/
System.out.println(OuterClass.a); // 可以直接访问外部类的静态变量
OuterClass.a1();
System.out.println(a); // 可以直接访问外部类的静态变量
a1();
// instanceVar = 20; // 错误!无法直接访问外部类的实例变量
/**
* 通过实例外部类的对象来访问外部类的实例成员
*/
OuterClass outerClass = new OuterClass();
System.out.println(outerClass.b);
outerClass.b1();
}
}
}
3.3、静态内部类可以包含静态成员变量和静态方法,这些静态成员与外部类的静态成员没有任何区别
public class OuterClass {
private static int staticVar;
public static class StaticInnerClass {
private static int innerStaticVar;
public static void innerStaticMethod() {
// ...
}
}
}
3.4、外部类可以通过(内部类.静态变量/方法)访问静态内部类的静态成员变量和静态方法,但不能直接访问静态内部类的实例变量和实例方法,需要通过静态内部类的实例来访问(包括private属性和方法)
public class OuterClass {
public static class StaticInnerClass {
//实例变量
private int innerVar;
//实例方法
private void innerMethod() {
// ...
}
//静态变量
private static int a = 10;
//静态方法
private static void a(){}
}
public void outerMethod() {
/**
* 通过实例静态内部类对象来 访问 静态内部类的实例变量/方法
*/
StaticInnerClass innerObj = new StaticInnerClass();
innerObj.innerVar = 10; // 外部类可以访问静态内部类的私有成员
innerObj.innerMethod(); // 外部类可以调用静态内部类的私有方法
/**
* 通过静态内部类.静态变量/静态方法 访问
*/
System.out.println(StaticInnerClass.a);
StaticInnerClass.a();
}
}
4、实例化静态内部类
实例化静态内部类时,不需要外部类的实例,可以直接使用静态内部类的类名进行实例化:
OuterClass.StaticInnerClass innerObj = new OuterClass.StaticInnerClass();
5、静态内部类的使用场景
- 封装性:将内部类声明为私有的,可以隐藏实现细节,只在外部类中暴露必要的接口。
- 组织性:将相关的类组织在一起,使代码结构更清晰。
- 静态工具类:当一个类只用于提供一些静态方法时,可以考虑使用静态内部类,这样可以将相关的方法集中在一个类中。
- 回调函数:可以将静态内部类作为回调函数的实现方式,简化代码的编写
总结:静态内部类是定义在另一个类内部的独立类,它与外部类的实例无关,可以在没有外部类对象的情况下被实例化。静态内部类具有自己的访问权限,并可以访问外部类的静态成员
四、方法内部类(Local Inner Class)
类似于局部变量,不可用访问权限修饰符修饰
它是定义在方法内部的类。方法内部类只在所属方法中可见,它具有方法的局部变量的生命周期,并且只能在所属方法中创建实例。
public class Test01 {
//静态变量
static String name;
//静态内部类
static class Inner1{
}
//实例变量
int age;
//实例内部类
class Inner2{
}
public void doSome(){
//局部变量
int i = 100;
//方法(局部)内部类
class Inner3{
}
}
public void doOther(){
//doSome()方法中的局部内部类Inner3,在doOther()方法中不能用
}
}
五、匿名内部类(Anonymous Inner Class)
1、匿名内部类是方法(局部)内部类中的一种。因为这个类没有名字而得名,叫做匿名内部类
2、不建议使用匿名内部类,因为一个类没有名字,就没有办法重复使用,另外代码太乱,可读性差
它是一种没有命名的内部类,用于创建一个临时的、只需使用一次的类。匿名内部类通常用于实现接口或继承抽象类,可以直接在创建对象的地方定义并实现它。
1、不使用匿名内部类
public class Test01 {
public static void main(String[] args) {
//充分考虑多态中的向上转型和静态绑定
ComputeImpl c1 = new ComputeImpl();
Compute c2 = new ComputeImpl();
MyMath mm = new MyMath();
mm.sum(c1,10,20);
mm.sum(c2,30,40);
}
}
//计算接口
interface Compute{
//抽象方法
int sum(int a,int b);
}
//数学类
class MyMath{
public void sum(Compute c,int x,int y){
int value = c.sum(x,y);
System.out.println(x + "+" + y + "=" + value);
}
}
//Computer类实现Compute接口
class ComputeImpl implements Compute{
public int sum(int a,int b){
return a + b;
}
}
2、使用匿名内部类
public class Test01 {
public static void main(String[] args) {
//充分考虑多态中的向上转型和静态绑定
MyMath mm = new MyMath();
//使用匿名内部类,表示这个ComputeImpt这个类没有名字了。
//这里表面看上去好像是接口可以直接new了,实际上并不是接口可以new了。
//后面的 {} 代表了对接口的实现
mm.sum(new Compute(){
public int sum(int a,int b){
return a + b;
}
},10,20);
//mm.sum(c2,30,40);
}
}
//计算接口
interface Compute{
//抽象方法
int sum(int a,int b);
}
//数学类
class MyMath{
public void sum(Compute c,int x,int y){
int value = c.sum(x,y);
System.out.println(x + "+" + y + "=" + value);
}
}
第八章、数组 Array (数据结构)
一、数组概述
1、数组是引用数据类型,父类是Object
在Java语言中,数组被归类为引用数据类型,而不是基本数据类型。基本数据类型包括整数、浮点数、布尔值等,而引用数据类型则包括类、接口、数组等。
当我们声明一个数组时,实际上是在创建一个引用类型的变量,该变量引用了存储在堆内存中的实际数组对象。这意味着数组变量本身存储的是对象的引用(内存地址),而不是实际的数据本身。
另外,数组的父类是Object
。在Java中,所有的类都直接或间接地继承自Object
类,因此数组也继承了Object
类的一些特性和方法。由于数组是Object
类的子类,因此可以使用Object
类中定义的方法,如toString()
、equals()
、hashCode()
等。
2、有序,固定长度,可同时容纳多个元素(容器)
数组一旦创建,在java中规定,长度不可变。
当我们说数组是一个容器时,意味着数组可以同时容纳多个元素。它是一个有序的、固定长度的数据集合,可以存储多个相同类型的元素。
在Java中,数组的长度是在创建时确定的,并且在创建后是固定的。一旦数组被创建,其长度就无法改变。
当你声明一个数组并为其分配内存空间时,需要指定数组的大小。这意味着你要在声明数组时明确指定数组的长度,例如 int[] numbers = new int[5];
,表示创建一个包含5个整数元素的数组。
2.1、无法对数组元素进行添加或删除
一旦数组被创建,其长度就不能再改变。你无法在现有数组中添加或删除元素,也无法改变数组的大小。如果你需要在运行时动态调整容量的数据结构,可以考虑使用ArrayList
等其他数据结构,它们提供了自动调整大小的功能。
然而,你可以通过创建一个新的数组来实现类似于调整大小的效果。你可以创建一个新数组,并将旧数组中的元素复制到新数组中。这个过程涉及到手动管理内存和元素复制的操作。
综上所述,Java中的数组长度是固定的,一旦创建后无法改变。如果你需要动态调整容量的数据结构,可以选择其他集合类或手动创建新数组来实现相应的功能。
3、存储在堆内存中
在Java中,所有的对象,包括数组对象,都存储在堆内存中。堆内存是用于动态分配对象的内存区域。当你创建一个数组对象时,Java会在堆内存中分配一块连续的内存空间来存储该数组的元素。
数组对象本身是一个引用类型,而不是实际的数据。数组变量存储的是对数组对象的引用(也称为指针),而不是实际的数据本身。这个引用指向分配在堆内存中的数组对象。
通过引用,我们可以访问和操作存储在堆内存中的数组对象。我们可以使用索引访问数组的元素,修改元素的值,获取数组的长度等。通过在堆内存中存储数组对象,Java提供了动态和灵活地管理和操作数组的能力。
需要注意的是,栈内存中的变量存储了对数组对象的引用,而不是直接存储整个数组对象。这意味着栈内存中的变量只存储了对堆内存中数组对象的引用地址,而不是实际的数据。
总结起来,Java中的数组对象存储在堆内存中,而数组变量存储在栈内存中。这种内存分配方式使得我们可以动态创建和操作数组对象,并使用引用来访问数组的元素和属性。
4、数组元素内存地址连续
数组中每一个元素的内存地址是连续的
5、首元素内存地址即数组对象内存地址
java中数组对象的首元素(从左到右第一个元素)的内存地址,作为整个数组对象的内存地址
因为数组中元素的内存地址是连续的,只要知道了第一个元素的内存地址,后面元素的内存地址都可以推算出来。
6、可索引
数组在内存中以连续的方式存储元素,每个元素占用相同的存储空间。这使得我们可以通过索引来访问和操作数组中的元素。索引是从0开始的整数,用于表示元素在数组中的位置。索引从左到右,以0开始,以1递增,最后一个元素的索引是 length - 1
java中数组的索引没有-1之说,只有从左到右,从0开始,依次加1
6.1、ArrayIndexOutOfBoundsException(数组索引越界异常)
int[] ints = {1,2,3};
//报错:java.lang.ArrayIndexOutOfBoundsException
System.out.println(ints[3]);
7、可以存储 基本数据类型 和 引用数据类型 数据
数组中如果
- 存储的是 java对象(引用数据类型)的话,实际上存储的是该对象的 引用 (内存地址)
- 存储的是基本数据类型的话,就是存储的基本数据类型的值本身
数组在Java中可以存储两种类型的数据:基本数据类型和引用数据类型。
-
基本数据类型数组:数组可以用于存储基本数据类型的值,例如整数、浮点数、字符等。在存储基本数据类型值的数组中,每个数组元素直接包含相应的值。例如,
int[] numbers = {1, 2, 3};
创建了一个存储整数类型的数组,其中每个元素直接存储整数值。 -
引用数据类型数组:数组也可以用于存储引用数据类型的值,例如类的对象、字符串等。在存储引用数据类型值的数组中,每个数组元素存储对应对象的引用(内存地址)。通过引用,我们可以访问和操作存储在堆内存中的对象。例如,
String[] names = {"Alice", "Bob", "Charlie"};
创建了一个存储字符串类型的数组,其中每个元素存储字符串对象的引用。
无论是基本数据类型数组还是引用数据类型数组,它们都可以包含多个元素,并通过索引进行访问和操作。数组的长度是在创建时指定的,一旦创建后,长度是固定的,不可更改。
需要注意的是,尽管数组可以存储引用数据类型的值,但它只存储了对象的引用,而不是对象本身。实际的对象是在堆内存中创建和存储的。因此,当操作引用数据类型数组时,需要注意引用的有效性和对象的生命周期。
总而言之,数组是一种通用的数据结构,既可以存储基本数据类型的值,也可以存储引用数据类型的值。这使得数组成为一种非常有用和灵活的数据容器,可用于存储和处理各种类型的数据。
需要注意的是,虽然数组是引用类型,但数组的元素可以是基本数据类型,例如
int
、char
等。例如:数组类型为Integer[] ,在这种情况下,数组中的元素会被封装为对应的包装类对象(如Integer
、Character
),从而符合引用类型的特性。
8、元素类型一致
在Java中,数组中存储的元素类型必须保持一致。也就是说,一个数组只能存储相同类型的元素。
当你声明一个数组时,需要指定数组的类型。这意味着该数组只能存储该指定类型的元素。例如,如果你声明一个整数数组,那么该数组只能存储整数类型的元素。类似地,如果你声明一个字符串数组,那么该数组只能存储字符串类型的元素。
这种类型一致性的要求是由Java语言的静态类型系统所决定的。它确保了在编译时和运行时,对数组元素的类型进行正确的访问和操作。
尝试存储不同类型的元素会导致编译错误。例如,如果你声明一个整数数组并尝试存储字符串类型的元素,编译器将报错。
如果你需要存储不同类型的元素,可以考虑使用对象数组(即存储引用数据类型)。通过使用对象数组,你可以存储不同类型的对象,因为所有对象都是通过它们的共同超类Object
进行引用的。然而,在这种情况下,你需要小心处理和类型转换以确保正确使用存储的对象。
总结起来,数组中存储的元素类型必须一致。这是Java语言的规定,它帮助我们在编写代码时保持类型安全性,并提供了更好的编译时检查和运行时行为。
9、数组中每个元素所占空间大小相同,因为每个元素类型相同
在Java中,每个数组元素所占用的空间大小取决于数组元素的类型。不同的数据类型占用的空间大小是不同的,且不一定是一个字节。
以下是Java中一些常见数据类型的空间大小:
- boolean:占用1个字节(8位)。
- byte:占用1个字节。
- char:占用2个字节。
- short:占用2个字节。
- int:占用4个字节。
- float:占用4个字节。
- long:占用8个字节。
- double:占用8个字节。
除了上述基本数据类型外,对象类型的数组元素还会占用额外的空间,因为它们存储的是对象的引用。
例如,如果有一个int
类型的数组,每个元素将占用4个字节。如果有一个double
类型的数组,每个元素将占用8个字节。
需要注意的是,Java的数组是对象,数组本身也需要额外的空间来存储有关数组的元信息,例如数组长度等。这个额外的空间大小在不同的实现中可能会有所不同。
因此,数组中每个元素所占用的空间大小取决于元素的类型,不一定是一个字节,而是根据类型的规定来确定的。
10、数组数据结构的优缺点
优点:
- 查询/查找/检索某个下标上的元素时效率极高。可以说是查询效率最高的一个数据结构
- 为什么检索效率高?
- 第一: 每一个元素的内存地址在空间存储上是连续的。
- 第二: 每一个元亲类型相同,所以占用空间大小一样。
- 第三:知道第一个元素内存地址,知道每一个元素占用空间的大小,又知道下标,所以通过一个数学表达式就可以计算出某个下标上元案的内存地址。直接通过内存地址定位元素,所以数组的检索效率是最高的。
- 数组中存储100个元素,或者存储100万个元素,在元亲查询/检索方面,效率是相同的,因为数组中元素查找的时候不会一个一个找,是通过数学表达式计算出来的。(算出一个内存地址,直接定位的。 )
缺点:
- 由于为了保证数组每个元素的内存地址连续,所以在数组上随机删除或者增加元素的时候,效率较低,因为随机增删元素会涉及到后面元素统一向前或者向后的操作
- 数组不能存储大数据量,因为很难找到一块特别大的内存空间
- 对于数组中最后一个元素的增删,是没有效率影响的
11、数组的特点
通过数组的容器特性,我们可以进行以下操作:
- 存储多个元素:数组可以同时容纳多个元素,并按照顺序将它们存储在内存中。这使得我们可以方便地存储和管理一组相关的数据。
- 访问数组元素:通过索引,我们可以快速访问数组中的特定元素。例如,要访问第一个元素,可以使用索引0;要访问第二个元素,可以使用索引1,以此类推。
- 修改数组元素:由于数组中的元素是可变的,我们可以通过索引来修改数组中的特定元素的值。这使得我们可以更新和改变数组中的数据。
- 遍历数组:通过循环结构,我们可以遍历整个数组,逐个访问和处理数组中的每个元素。这在处理大量数据时非常有用。
- 数组操作:数组提供了许多操作方法,如排序、搜索、插入、删除等。这些操作使我们能够对数组中的元素进行排序、查找特定元素以及在指定位置插入或删除元素。
通过数组作为容器,我们可以方便地组织和处理多个相同类型的数据。它是一种简单而强大的数据结构,被广泛用于各种编程场景和算法实现。
二、数组的分类
在Java中,数组可以根据维度的不同进行分类,包括一维数组、二维数组、三维数组和多维数组。
1、一维数组(One-dimensional Array)
一维数组是最简单的数组形式,它包含一系列按顺序排列的元素。一维数组可以表示为一个线性结构,其中每个元素通过索引进行访问。例如,int[] numbers = {1, 2, 3, 4, 5};
就是一个一维整数数组。
2、二维数组(Two-dimensional Array)
二维数组是一个表格形式的数组,它由行和列组成。可以将二维数组看作是一个矩阵,其中每个元素由两个索引(行索引和列索引)进行访问。例如,int[][] matrix = {{1, 2, 3}, {4, 5, 6}};
表示一个包含两行三列的二维整数数组。
3、三维数组(Three-dimensional Array)
三维数组是一个由多个二维数组组成的数组。它可以看作是一个立方体或者多个平面组成的立体结构。三维数组需要使用三个索引来访问元素,分别表示层数、行数和列数。例如,int[][][] cube = {{{1, 2}, {3, 4}}, {{5, 6}, {7, 8}}};
是一个包含两个二维数组的三维整数数组。
4、多维数组(Multidimensional Array)
多维数组是包含多个维度的数组,它可以有任意多个维度。除了一维数组、二维数组和三维数组之外,还可以有四维数组、五维数组等等。多维数组的访问需要使用对应的索引进行定位。例如,int[][][][] multidimensionalArray = new int[2][3][4][5];
是一个包含四个维度的多维整数数组。
通过使用多维数组,我们可以更灵活地组织和处理复杂的数据结构。每个维度的大小决定了数组的容量和结构。根据需求,我们可以选择合适的数组维度来存储和操作数据。
三、数组的通用属性
数组对象在Java中具有以下属性:
1、长度(length)
数组对象的长度是指它包含的元素数量。它表示数组在创建时指定的固定大小。你可以使用length
属性来获取数组的长度。例如,int[] numbers = {1, 2, 3};
中的数组长度为3,可以通过numbers.length
获取。
2、类型(type)
数组对象的类型是指它所能存储的元素类型。数组的类型由声明数组时指定的类型确定。例如,int[] numbers = new int[5];
中的数组类型是int
,它只能存储整数类型的元素。
这些属性是数组对象的固有属性,可以在程序中使用它们来获取有关数组的相关信息。例如,使用数组的长度属性可以确定数组的大小,而使用数组的类型属性可以确保我们只存储正确类型的元素。
需要注意的是,这些属性是数组对象的成员属性,而不是方法。因此,我们可以直接通过访问数组对象的属性来获取相关信息,而不需要调用方法。
示例代码:
int[] numbers = {1, 2, 3, 4, 5};
System.out.println("数组长度:" + numbers.length);
System.out.println("数组类型:" + numbers.getClass().getComponentType().getSimpleName());
输出结果:
数组长度:5
数组类型:int
总结:数组对象具有长度和类型这两个主要的属性。长度表示数组包含的元素数量,类型表示数组能够存储的元素类型。通过访问这些属性,我们可以获取有关数组的相关信息并进行相应的处理。
四、一维数组
一维数组是Java中最基本的数据结构之一,它是一组按照顺序排列的相同类型的元素集合。在Java中,一维数组可以存储基本数据类型(如整数、浮点数等)或引用类型(如对象)。
1、声明和创建一维数组
要声明一个一维数组,需要指定数组的类型和名称,并使用方括号 [] 表示数组的维度。
语法:
int[ ] array1; //c风格:int array1 [ ]
double[ ] array2; //c风格:double array2[ ]
boolean[ ] array3; //c风格:boolean array3[ ]
String[ ] array4; //c风格:String array4[ ]
Object[ ] array5; //c风格:Object array5[ ]
2、初始化一维数组
若数据类型为引用数据类型,则可以存储该类型的实例对象及该类型的子类的实例对象
2.1、静态(显示)初始化
可以在创建数组时为数组元素赋初始值
语法:
类型[ ] 引用名 = {元素1,元素2,元素3,...... }; //须用花括号
类型 引用名[ ] = {元素1,元素2,元素3,...... }; //c风格
int[] numbers = {1, 2, 3, 4, 5};
演示静态初始化:
package com.javase.数组;
public class ArrayTest01 {
public static void main(String[] args) {
//声明一个int类型的数组,使用静态初始化方法
int[] numbers = {1, 2, 3, 4, 5};
//所有的数组对象都有length属性
System.out.println("数组中元素个数: " + numbers.length);
//数组中每个元素都有下标
//通过索引堆数组中的元素进行存取操作
//访问数组中元素
System.out.println("第一个元素:" + numbers[0]);
System.out.println("最后一个元素:" + numbers[numbers.length - 1]);
//修改数组中元素
numbers[0] = 111;
numbers[numbers.length - 1] = 222;
System.out.println("第一个元素:" + numbers[0]);
System.out.println("最后一个元素:" + numbers[numbers.length - 1]);
//遍历数组中元素
//从第一个元素变量到最后一个元素
for (int i = 0; i < numbers.length; i++) {
System.out.println("第" + (i + 1) + "位置处元素为:" + numbers[i]);
}
//从最后一个元素遍历到第一个元素
for(int i = numbers.length - 1;i >= 0;i--){
System.out.println("第" + (i + 1) + "位置处元素为:" + numbers[i]);
}
}
}
2.2、静态(匿名)初始化
可以在需要的地方使用匿名数组初始化,这可以帮助您在一些特定情况下更灵活地初始化数组,例如作为方法参数、赋值等。
int[] dynamicArray = new int[]{10, 20, 30};
Object[] mixedArray = new Object[]{1, "two", 3.0};
2.3、动态(默认)初始化
默认初始化:在创建数组时,如果没有为数组元素赋初始值,Java会对数组进行默认初始化。
- byte[ ],元素默认初始化为0;
- short[ ],元素默认初始化为0;
- int[ ],元素默认初始化为0;
- long[ ],元素默认初始化为0L;
- float[ ],元素默认初始化为0.0F;
- double[ ],元素默认初始化为0.0;
- boolean[ ],元素默认初始化为false;
- char[ ],元素默认初始化为 \u0000;
- 对于引用类型数组,元素默认初始化为null。
语法:
类型[ ] 引用名 = new 类型[ 元素个数];
类型 引用名[ ] = new 类型[ 元素个数];//c风格
下面是一个声明和创建一个包含整数的一维数组的示例:
int[] numbers = new int[5];
上述代码声明了一个名为 numbers
的整数数组,它可以存储5个整数。使用 new
关键字创建了一个数组对象,并指定了数组的长度为5。且每个元素默认值为 0
也可以将数组声明和创建拆分成两个步骤,如下所示:
int[] numbers; // 声明数组
numbers = new int[5]; // 创建数组
演示动态初始化:
package com.javase.数组;
public class ArrayTest02 {
public static void main(String[] args) {
//声明定义一个数组,采用动态初始化的方式创建
int[] a = new int[4];
//变量数组
for(int i = 0;i < a.length;i++){
System.out.println("数组中索引为" + i + "的数组是" + a[i]); //默认初始化为0
}
//后期赋值
a[0] = 1;
a[1] = 100;
for(int i = 0;i < a.length;i++){
System.out.println("数组中索引为" + i + "的数组是" + a[i]); //后期进行赋值,将第一个修改为1,将第二个修改为100
}
//初始化一个Object类型的数组,采用动态初始化
Object[] objs = new Object[3]; //3个长度,因为是引用类型,每个元素默认为null
for(int i = 0;i < objs.length;i++){
System.out.println("数组中索引为" + i + "的数组是" + objs[i]);
}
//初始化一个Object类型的数组,采用静态初始化
Object o1 = new Object();
Object o2 = new Object();
Object o3 = new Object();
Object[] objects = {o1,o2,o3};
for (int i = 0; i < objects.length; i++) {
System.out.println(objects[i]); //会自动调用toString方法,输出该对象的内存地址(十六进制)
}
System.out.println("=============================================");
//初始化一个String类型的数组,采用动态初始化
String[] strings = new String[4];
for (int i = 0; i < strings.length; i++) {
System.out.println(strings[i]); //默认初始化为null
}
//初始化一个String类型的数组,采用静态初始化
String[] strings1 = {"a","b","c"};
for (int i = 0; i < strings1.length; i++) {
System.out.println(strings1[i]);
}
}
}
2.4、初始化的使用条件
什么时候采用静态初始化方式,什么时候采用动态初始化方式?
1、当创建数组对象时,确定数组中存储哪些具体的元素时,采用静态初始化方式。
2、当创建数组对象时,不确定将来数组中存储哪些数据,可以采用动态初始化的方式,预先分配内存空间
3、访问一维数组元素
可以使用索引访问一维数组中的元素。数组的索引从0开始,最大索引为数组长度减1。例如,要访问 numbers
数组的第一个元素,可以使用索引0:
int firstNumber = numbers[0];
注意,访问数组元素时,索引必须在有效范围内,否则会引发
ArrayIndexOutOfBoundsException
异常。
4、修改一维数组元素
可以通过索引来修改一维数组中的元素的值。例如,要将 numbers
数组的第二个元素修改为10:
numbers[1] = 10;
注意,修改数组元素时,索引必须在有效范围内,否则会引发
ArrayIndexOutOfBoundsException
异常。
5、获取一维数组长度
可以使用数组的 length
属性来获取一维数组的长度,即数组中元素的个数。例如:
int length = numbers.length;
6、遍历一维数组
可以使用循环结构(如 for
循环或 foreach
循环)来遍历一维数组中的元素。以下是使用 for
循环遍历 numbers
数组的示例:
for (int i = 0; i < numbers.length; i++) {
System.out.println(numbers[i]);
}
7、数组可作为方法参数使用
package com.javase.数组;
public class ArrayTest03 {
public static void main(String[] args) {
//java风格
int[] a1 = {1,2,3};
for (int i = 0; i < a1.length; i++) {
System.out.println(a1[i]);
}
System.out.println("=====================================");
//还可以采用c++的语法格式
int a2[] = {4,5,6};
for (int i = 0; i < a2.length; i++) {
System.out.println(a2[i]);
}
System.out.println("====================================");
//创建int数组
int[] x = {1,2,3,4,5};
ArrayTest03.printArry(x);
System.out.println("=====================================");
//创建String数组
String[] y = {"j","z","q"};
printArry(y);
System.out.println("=====================================");
String[] strArray = new String[5];
printArry(strArray); //输出5个null
System.out.println("=====================================");
printArry(new String[4]); //输出4个null
System.out.println("=====================================");
printArry(new int[3]); //输出3个0
}
//方法重载:分别定义输出整数数组和字符串数组的方法
public static void printArry(int[] numbers){
for (int i = 0; i < numbers.length; i++) {
System.out.println(numbers[i]);
}
}
public static void printArry(String[] strs){
for (int i = 0; i < strs.length; i++) {
System.out.println(strs[i]);
}
}
}
7.1、不允许直接在方法参数中进行数组静态初始化
package com.javase.数组;
public class ArrayTest04 {
public static void main(String[] args) {
//静态初始化
int[] a = {1,2,3};
printArray(a);
System.out.println("===============================");
//没有这种语法
//printArray({1,2,3});
//如果直接传递一个静态数组的话,语法应这样写
printArray(new int[]{4,5,6});
System.out.println("===============================");
//动态初始化
int[] a2 = new int[4];
printArray(a2);
System.out.println("===============================");
printArray(new int[2]);
}
public static void printArray(int[] numbers){
for (int i = 0; i < numbers.length; i++) {
System.out.println(numbers[i]);
}
}
}
7.2、main方法 String[] args数组详解
package com.javase.数组;
/**
* main方法上面的String[] args 详解
*
* 由JVM负责调用main方法
* JVM调用main方法的时候,会自动传一个String数组对象
*/
public class ArrayTest05 {
//这个方法是程序员写出来的,JVM负责调用,JVM在调用的时候一定会传一个String数组过来
public static void main(String[] args) {
//JVM默认传递过来这个数组对象长度为:0,默认为0
//通过测试得知:args不是null
System.out.println("JVM给传递的String数组参数长度为:" + args.length);
//这个数组什么时候会有值尼
//其实这个数组是留给用户的,用户可以在控制台上输入参数,这个参数会自动被转换
//例如这样运行程序,java ArrayTest05 a b c
//那么这个时候JVM会自动将“a b c”,通过空格的方式进行分离,分离完成之后,自动放到String[] args数组中
//所以main方法上面的String[] args数组主要是用来接受用户输入参数的
//把“a b c”转换成字符串数组:{“a”,"b","c"}
//遍历数组
for (int i = 0; i < args.length; i++) {
System.out.println(args[i]);
}
System.out.println("========================================");
//以下这一行代码表示的含义,数组对象创建了,但是数组中没有任何数据
printLength(new String[0]); //0
printLength(new String[]{}); //0 静态初始化,里面没东西
}
public static void printLength(String[] args){
System.out.println(args.length);
}
}
IDE 带参数运行程序
7.3、登录案例
package com.javase.数组;
public class ArrayTest06 {
//用户名和密码输入到String[] args数组当中
public static void main(String[] args) {
if (args.length != 2) {
System.out.println("使用该系统时请输入程序参数,参数中包括用户名和密码。");
return;
}
//程序执行到此处说明用户提供了用户名和密码
//接下来应该判断用户名和密码是否正确
//取出用户名
String username = args[0];
//取出密码
String password = args[1];
//假设用户名是admin,密码是123的时候,登录成功,其他一律失败
//判断两个字符串是否相等,使用equals方法
// if(username.equals("admin") && password.equals("123")){
//采用以下编写方式,可以避免空指针异常
if("admin".equals(username) && "123".equals(password)){
System.out.println("登录成功,欢迎" + username + "回来!");
}else {
System.out.println("验证失败,用户名或密码错误");
}
}
}
8、一维数组存储引用数据类型
8.1、数组的声明和创建
SomeClass[] arrayName; // 声明一个 SomeClass 类型的数组变量
arrayName = new SomeClass[length]; // 创建一个长度为 length 的 SomeClass 类型的数组
8.2、初始化数组元素
arrayName[index] = new SomeClass(); // 初始化数组的第 index 个元素为 SomeClass 的对象实例
8.3、相关操作
package com.javase.数组;
//一维数组的深入,数组中存储的类型为:用于数据类型
public class ArrayTest07 {
public static void main(String[] args) {
//静态初始化一个Animal类型的数组
Animal a1 = new Animal();
Animal a2 = new Animal();
Animal[] animals = {a1,a2};
//遍历Animal[]
for (int i = 0; i < animals.length; i++) {
animals[i].move(); //是数组中Animal 对象的方法
}
//动态初始化一个长度为2的Animal数组
Animal[] animals1 = new Animal[2];
animals1[0] = new Animal();
//Animal[]类型数组中只能存放Animal类型,不能存放Product类型
//animals1[1] = new Product();
//Animal[]类型数组中可以存放Cat类型,因为Cat是Animal类型的子类,自动类型转换,向上转型
animals1[1] = new Cat();
System.out.println("==========================================");
//创建一个Animal类型数组,存放Cat和Bird
Animal[] animals2 = {new Cat(),new Bird()};
for (int i = 0; i < animals2.length; i++) {
//这个取出来的有可能是Cat,也有可能是Bird,一定是Animal
//如果调用的方法是父类中存在的方法不需要向下转型,直接使用父类型引用调用即可
Animal a = animals2[i];
/**
* 猫在走猫步
* Bird fly!
*/
a.move();
//Animal中没有sing方法,须向下转型
//animals2[i].sing();
//调用子类中特有方法,须向下转型
if(a instanceof Cat){
Cat c = (Cat) a;
c.catchMouse(); //抓老鼠!
} else if (a instanceof Bird) {
Bird b = (Bird) a;
b.sing(); //鸟儿在唱歌!
}
}
}
}
class Animal{
public void move(){
System.out.println("Animal move...");
}
}
class Cat extends Animal{
public void move(){
System.out.println("猫在走猫步");
}
//特有的方法
public void catchMouse(){
System.out.println("抓老鼠!");
}
}
class Bird extends Animal{
public void move(){
System.out.println("Bird fly!");
}
//特有方法
public void sing(){
System.out.println("鸟儿在唱歌!");
}
}
class Product{
}
9、数组扩容 System.arraycopy( )
在 Java 中,一维数组是固定长度的,一旦创建后,其长度就无法更改。但是可以通过创建一个新的数组来模拟扩容的效果。下面是一种常见的方法来实现一维数组的扩容:
9.1、创建原始数组:
int[] originalArray = new int[5]; // 原始数组长度为 5
9.2、创建新的扩容数组:
int newLength = originalArray.length * 2; // 新数组长度为原始数组的两倍
int[] newArray = new int[newLength]; // 创建新的数组
9.3、复制原始数组到新数组:
System.arraycopy(originalArray, 0, newArray, 0, originalArray.length);
9.4、更新引用指向新数组:
originalArray = newArray;
通过以上步骤,我们创建了一个新的长度为原始数组两倍的数组,并将原始数组的元素复制到新数组中。最后,将原始数组的引用指向新数组,从而实现了一维数组的扩容。
需要注意的是,扩容数组时,原始数组中的元素会被复制到新数组中,但是新数组多出来的部分会被默认初始化为相应数据类型的默认值(例如,整型数组中多出的元素会被初始化为 0)。
另外,Java 中也提供了更方便的集合类(如 ArrayList
)来代替一维数组,这些集合类具有动态扩容的能力,并且提供了更多的操作和方法。如果需要频繁地进行数组元素的添加和删除操作,建议使用集合类来代替一维数组。
package com.javase.数组;
/**
* 关于一维数组的扩容
* 在java开发中,数组长度一旦确定不可变,那么数组满了怎么办?
* 数组满了,需要扩容
* java中对数组的扩容是:
* 先创建一个大容量的数组,然后将小容量数组中的数据一个个拷贝到大数组中。
*
* 结论:
* 数组扩容效率较低,因为涉及到拷贝的问题,所以在以后的开发中请注意,尽可能少的进行数组的拷贝。
* 可以在创建数组对象的时候估计一下多少合适,最好预估准确,这样可以减少数组的扩容次数。提高效率
*/
public class ArrayTest08 {
public static void main(String[] args) {
//拷贝源,从这个数组中拷贝
int[] src = {1,11,22,3,4};
//拷贝目标,拷贝到这个数组上
int[] dest = new int[20]; //动态初始化一个长度为20的数组
//拷贝部分元素
//调用System类中的arraycopy方法
//System.arraycopy(src,1,dest,3,2);
//变量目标数组
/*for (int i = 0; i < dest.length; i++) {
System.out.println(dest[i]); //0 0 0 11 22 ......0
}*/
//拷贝全部元素
System.arraycopy(src,0,dest,0,src.length);
for (int i = 0; i < dest.length; i++) {
System.out.println(dest[i]); //0 0 0 11 22 ......0
}
System.out.println("===============================");
//拷贝引用数据类型数组
String[] strings = {"a","b","c"};
String[] strings1 = new String[10];
System.arraycopy(strings,0,strings1,0,strings.length);
for (int i = 0; i < strings1.length; i++) {
System.out.println(strings1[i]); //自动调用toString()方法
}
System.out.println("===============================");
Object[] objects = {new Object(),new Object(),new Object()};
Object[] objects1 = new Object[5];
System.arraycopy(objects,0,objects1,0,objects.length);
objects = objects1;
for (int i = 0; i < objects.length; i++) {
System.out.println(objects1[i]); //自动调用toString()方法
}
System.out.println(src);
}
}
9.5、内存图分析
基本数据类型数组内存分析
引用数据类型数组内存分析
五、二维数组
在Java中,二维数组是一个由多个一维数组组成的数组。它实际上是一个表格或矩阵,其中的元素可以通过行索引和列索引来访问。
二维数组其实是一个特殊的一维数组,特殊在这个二维数组中的每一个元素都是一个一维数组
1、声明和创建二维数组
数据类型[][] 数组名; //java风格
或
数据类型 数组名[][]; //c风格
这里的 "数据类型" 可以是任何Java数据类型,包括原始类型(如int、double等)或引用类型(如String、对象等)。你还可以使用 int[][]
或 int[][][]
等形式声明多维数组。
2、初始化二维数组
若数据类型为引用数据类型,则可以存储该类型的实例对象及该类型的子类的实例对象
2.1、静态(显示)初始化
语法:
数据类型[ ][ ] 引用名 = {
{元素,......},
{元素,......},
......
};
c风格只是 数据类型 引用名 [ ][ ] 其余一样
你可以在创建二维数组时进行初始化,也可以分别为每个元素赋值。以下是一些示例:
int[][] matrix = { {1, 2, 3}, {4, 5, 6}, {7, 8, 9} };
这将创建一个3行3列的二维整数数组,并将其初始化为指定的值。
2.2、动态(默认)初始化
语法:
数据类型[ ][ ] 引用名 = new 数据类型[ 行数 ][ 列数 ];
数据类型 引用名[ ][ ] = new 数据类型[ 行数 ][ 列数 ]; //c风格
这会创建一个具有指定行数和列数的二维数组。例如:
int[][] matrix = new int[3][4];
这将创建一个包含3行4列的二维整数数组。
另一种初始化方式是使用循环为每个元素赋值:
int[][] matrix = new int[3][3];
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[i].length; j++) {
matrix[i][j] = i + j;
}
}
案例
package com.javase.数组;
public class ArrayTest12 {
public static void main(String[] args) {
//3行4列
//3个一维数组,每个一维数组中4个元素
int[][] ints = new int[3][4];
//二维数组变量
/*for (int i = 0; i < ints.length; i++) {
for (int j = 0; j < ints[i].length; j++) {
System.out.print(ints[i][j] + " ");
}
System.out.println();
}*/
//静态初始化
int[][] ints1 = {
{1,2,3},
{4,5,6,7},
{8,9}
};
printArray(ints1);
//没有这种语法
/*printArray({
{1,2,3},
{4,5,6,7},
{8,9}
});*/
//可以这样写
printArray(new int[][]{
{1,2,3},
{4,5,6,7},
{8,9}
});
}
public static void printArray(int[][] array){
for (int i = 0; i < array.length; i++) {
for (int j = 0; j < array[i].length; j++) {
System.out.print(array[i][j] + " ");
}
System.out.println();
}
}
}
3、二维数组的length属性
数据类型[ ][ ] 数组名;
数组名.length 表示的是整个二维数组中一维数组的个数,即行数。
数组名[索引].length 表示的是二维数组中一维数组中的元素的个数,即列数。
package com.javase.数组;
public class ArrayTest09 {
public static void main(String[] args) {
//一维数组
int[] ints = {100,200,300};
System.out.println(ints.length); //3
//二维数组
//里面是4个一维数组
int[][] ints1 = {
{100,200,300},
{30,20,40,50,60},
{6,7,9,1},
{0}
};
System.out.println(ints1.length); //4
for (int i = 0; i < ints1.length; i++) {
System.out.println("第" + i + "索引处的数组的元素个数为:" + ints1[i].length); //3,5,4,1
}
}
}
4、访问二维数组元素
你可以使用行索引和列索引来访问二维数组中的元素。索引从0开始。例如:
1、前面一个索引表示二维数组中的一维数组的位置索引,后面一个索引表示该一维数组中的元素索引
2、matrix[1]是一个整体,[2]取具体元素索引
int value = matrix[1][2];
这将返回二维数组中第2行、第3列的元素。
5、修改二维数组元素
数据类型[行索引][列索引] = 值;
package com.javase.数组;
public class ArrayTest10 {
public static void main(String[] args) {
//二维数组
int[][] ints = {
{1,2,3},
{4,5,6,7},
{90,100}
};
int[] a0 = ints[0];
System.out.println("第一个一维数组中的第一个元素:" + a0[0]);
System.out.println("第一个一维数组中的第一个元素:" + ints[0][0]);
System.out.println("第二个一维数组中的第三个元素:" + ints[1][2]);
System.out.println("第三个一维数组中的第一个元素:" + ints[2][0]);
//修改元素
ints[1][2] = 123;
//索引越界异常
//java.lang.ArrayIndexOutOfBoundsException
System.out.println(ints[2][9]);
}
}
6、遍历二维数组
可以使用嵌套循环来遍历二维数组的所有元素。例如:
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[i].length; j++) {
int value = matrix[i][j];
System.out.print(value + " ");
}
System.out.println(); // 换行
}
这将按行打印二维数组中的所有元素。
package com.javase.数组;
public class ArrayTest11 {
public static void main(String[] args) {
//二维数组
String[][] strings = {
{"java","python","JavaScript","TypeScript"},
{"张三","李四","王五"},
{"jack","lucy"}
};
//遍历二维数组
for (int i = 0; i < strings.length; i++) { //该循环循环3次,为行数
//内层循环
String[] strings1 = strings[i];
for (int j = 0; j < strings1.length; j++) {
System.out.println("第" + i + "行的第" + j + "个元素为:" + strings[i][j] );
}
}
//合并代码
for (int i = 0; i < strings.length; i++) { //该循环循环3次,为行数
//内层循环
for (int j = 0; j < strings[i].length; j++) {
System.out.print(strings[i][j] + ' ');
}
System.out.println();
}
}
}
二维数组在处理矩阵、图像等问题时非常有用。它提供了一种方便的方式来组织和操作多个数据元素。
六、案例详解
1、一维数组模拟栈数据结构
编写程序:使用一维数组,模拟栈数据结构
要求:
1、这个栈可以存储java中的任何引用数据类型
2、在栈中提供push方法模拟压栈。(栈满了,要有提示信息)
3、在栈中提供pop方法模拟弹栈。(栈空了,也要有提示信息)
4、编写测试程序,new栈对象,调用push,pop方法模拟压栈和弹栈动作。
5、栈的默认初始化容量为10(注意,无参数构造方法设置)
package com.javase.数组.数组模拟栈数据结构;
/**
* 编写程序:使用一维数组,模拟栈数据结构
* 要求:
* 1、这个栈可以存储java中的任何引用数据类型
* 2、在栈中提供push方法模拟压栈。(栈满了,要有提示信息)
* 3、在栈中提供pop方法模拟弹栈。(栈空了,也要有提示信息)
* 4、编写测试程序,new栈对象,调用push,pop方法模拟压栈和弹栈动作。
*/
public class MyStack {
/**
* 向栈当中存储元素,我们这里使用一维数组模拟,存到栈中,就表示存储到数组中
* 因为数组是我们学习java的第一个容器
* 为什么选择Object类型数组?因为这个栈可以存储java中任何引用类型数据
*/
private Object[] objects;
//setters and getters
public Object[] getObjects() {
return objects;
}
public void setObjects(Object[] objects){
this.objects = objects;
}
/**
* 模拟栈顶指针,永远指向栈顶元素
* 默认初始值为0,注意:最初的栈是空的,一个元素也没有
*
* 如果默认初始化为0,则表示栈顶指针指向了顶部元素的上方
* 如果默认初始化为-1,则表示栈顶指针指向了顶部元素
*/
private int index;
//setters and getters
public int getIndex() {
return index;
}
public void setIndex(int index) {
this.index = index;
}
/**
* 构造方法,构造方法执行的时候进行初始化
*/
public MyStack(){
//默认初始化容量为10
this.objects = new Object[10];
//默认初始化栈顶指针为-1
this.index = -1;
}
//有参构造
public MyStack(int num){
this.objects = new Object[num];
}
/**
* 提供一个压栈的方法
* @param obj 要压的元素
*/
public void push(Object obj){
if(this.getIndex() >= this.getObjects().length - 1){
System.out.println("压栈失败,栈已满!");
return;
}
this.setIndex(this.getIndex() + 1);
this.getObjects()[this.getIndex()] = obj;
//所有的System.out.println方法执行时,如果输出的引用的话,会自动调用引用的toString方法
System.out.println("压栈" + obj + "成功,栈顶指针指向:" + this.getIndex());
}
/**
* 弹栈的方法,往外取元素
* @return 返回当前弹出的元素
*/
public Object pop(){
if(this.getIndex() < 0){
System.out.println("弹栈失败,栈已空");
return null;
}
System.out.print("弹栈" + this.getObjects()[this.getIndex()] + "元素成功,");
//栈顶指针向下移动一位
this.setIndex(this.getIndex() - 1);
System.out.print("栈顶指针指向" + this.getIndex());
System.out.println();
//程序执行到此处说明栈没有空
return this.getObjects()[this.getIndex() + 1];
}
/*public Object aa(){
return;
}*/
}
class TestMyStack{
public static void main(String[] args) {
//创建一个栈对象
MyStack ms = new MyStack();
for (int i = 0; i < 11; i++) {
ms.push(new Object());
}
// Object a1 = ms.pop();
// System.out.println(a1);
// Object a2 = ms.pop();
// System.out.println(a2);
for (int i = 0; i < 11; i++) {
ms.pop();
}
}
}
2、二维数组模拟酒店管理系统
为某个酒店编写程序,酒店管理系统,模拟订房,退房,打印所有房间状态等功能。
1、该系统的用户是:酒店前台
2、酒店使用一个二维数组来模拟。
3、酒店中的每一个房间应该是一个java对象
4、每一个房间Room应该有:房间编号,房间类型,房间是否空闲
5、系统应该堆外提供的功能:
可以预定房间,用户输入房间编号,订房
可以退房,用户输入房间编号,退房
可以查看所有房间的状态,用户输入某个指令应该可以查看所有房间状态
Room.java
package com.javase.数组.酒店管理系统;
/**
* 酒店房间
*/
public class Room {
/**
* 房间编号
* 1楼:101 102 103 ......
* 2楼:201 202 203 ......
* 3楼:301 302 303 ......
*/
private int id;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
/**
* 房间类型:标准间,单人间,总统套房
*/
private String type;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
/**
* 房间状态
* true:表示空闲,可以预定
* false:表示占用,不能被预定
*/
private boolean status;
//idea对于boolean类型的变量,生成的get方法的方法名:isXXX()
public boolean getStatus() {
return status;
}
public void setStatus(boolean status) {
this.status = status;
}
/**
* 构造方法
*/
public Room() {
}
public Room(int id, String type, boolean status) {
this.id = id;
this.type = type;
this.status = status;
}
/**
* 重写equals方法
*/
@Override
public boolean equals(Object obj){
if(obj == null || !(obj instanceof Room)){
return false;
}
if(this == obj){
return true;
}
Room room = (Room) obj;
//当前房间编号等于传过来的房间的编号,则认为是同一个房间
return this.getId() == room.getId();
}
/**
* 重写toString方法
*/
@Override
public String toString(){
return "[ 房间编号为:" + this.getId() + ",房间状态为:" + (this.getStatus()? "空闲" : "占用") + ",房间类型为:" + this.getType() + " ]";
}
}
class TestRoom{
public static void main(String[] args) {
Room room1 = new Room(1,"标间",true);
Room room2 = new Room(1,"标间1",true);
System.out.println(room1);
System.out.println(room1.equals(room2));
}
}
Hotel.java
package com.javase.数组.酒店管理系统;
/**
* 酒店对象,酒店中有二维数组,二维数组模拟大厦
*/
public class Hotel {
/**
* 二维数组,模拟大厦所有的房间
*/
private Room[][] rooms;
/**
* 构造方法
* 通过构造方法来盖楼
*/
public Hotel(){
//一共有几层,每层的房间类型,编号等
//可以先写死,一共有三层,一层单人间,二层标准间,三层套房
/**
* 房间编号
* 1楼:101 102 103 ......
* 2楼:201 202 203 ......
* 3楼:301 302 303 ......
*/
//动态初始化
rooms = new Room[3][10]; //3行10列,3层楼,每层10个房间
//创建30个Room对象
for (int i = 0; i < rooms.length; i++) {
for (int j = 0; j < rooms[i].length; j++) {
if(i == 0){
//一层
rooms[i][j] = new Room((i + 1) * 100 +(j + 1),"单人间",true);
} else if (i == 1) {
//二层
rooms[i][j] = new Room((i + 1) * 100 +(j + 1),"标准间",true);
}else {
//三层
rooms[i][j] = new Room((i + 1) * 100 +(j + 1),"套房",true);
}
}
}
}
/**
* 打印房间列表的方法
* 遍历二维数组
*/
public void print(){
for (int i = 0; i < rooms.length; i++) {
for (int j = 0; j < rooms[i].length; j++) {
System.out.print(rooms[i][j].toString());
}
System.out.println();
}
}
/**
* 订房方法
* @param roomId 需要传递房间编号
*/
public void order(int roomId){
//订房最主要的是将房间对象的status修改为false
//通过房间编号推算出索引,修改状态
Room room = rooms[(roomId / 100) - 1][(roomId % 100) - 1];
//表示房间占用
room.setStatus(false);
System.out.println(roomId + "定房成功!");
}
/**
* 退房方法
* @param roomId 需要传入房间编号
*/
public void exit(int roomId){
Room room = rooms[(roomId / 100) - 1][(roomId % 100) - 1];
//表示房间空闲
room.setStatus(true);
System.out.println(roomId + "退房成功!");
}
}
HotelMgSystem.java
package com.javase.数组.酒店管理系统;
import java.util.Scanner;
public class HotelMgtSystem {
public static void main(String[] args) {
System.out.println(new HotelMgtSystem()); //com.javase.数组.酒店管理系统.HotelMgtSystem@7ef20235
//创建酒店对象
Hotel hotel = new Hotel();
/**
* 首先输出欢迎页面
*/
System.out.println("欢迎使用酒店管理系统,请认真阅读以下使用说明");
System.out.println("请输入对应的功能编号:[1] 表示查看房间列表,[2] 表示订房,[3] 表示退房,[0]表示退出系统");
Scanner input = new Scanner(System.in);
while (true){
System.out.print("请输入功能编号:");
int i = input.nextInt();
if(i == 1){
//查看房间列表
hotel.print();
} else if (i == 2) {
//订房
System.out.print("请输入定房编号:");
int roomId = input.nextInt();
hotel.order(roomId);
} else if (i == 3) {
//退房
System.out.print("请输入退房编号:");
int roomId = input.nextInt();
hotel.exit(roomId);
} else if (i == 0) {
//退出系统
System.out.println("欢迎下次使用!");
break;
}else {
//输入错误
System.out.println("输入功能编号有误,请重新输出!");
}
}
}
}
七、冒泡排序(排序算法)
冒泡排序(Bubble Sort)是一种简单的排序算法,它重复地遍历待排序的元素列表,依次比较相邻的两个元素,并将顺序不正确的元素进行交换,使较大的元素逐渐"浮"到列表的末尾,最终实现整个列表的排序。
每一次循环结束后,都要找出最大的数据,放到参与比较的这堆数据的最右边。(冒出最大的那个气泡。)
核心:拿着左边的数字和右边的数字比较,当左边的数字 > 右边的数字的时候,交换位置。
以下是冒泡排序算法的详细步骤:
-
首先,从待排序的列表中选择第一个元素作为当前元素。
-
比较当前元素与它的下一个元素,如果当前元素大于下一个元素,则交换它们的位置,使较大的元素向后移动。
-
继续比较下一个相邻元素,并执行相同的交换操作,直到遍历到列表的倒数第二个元素。
-
一轮遍历后,最大的元素已经"浮"到列表的末尾。
-
重复执行步骤2至步骤4,直到所有元素都被排序。
1、原理分析
原始数据:3 ,2 ,7 ,6 ,8,原始数据个数为5
5个数据,循环4次
参与比较的数据:3 ,2 ,7 ,6 ,8
第1次循环:(原始数据个数 - 0次循环 = 现有数据个数,5个数据,比较4次)
- 2 ,3 ,7 ,6 ,8(第1次比较:3和2比较,3 > 2 ,交换位置)
- 2 ,3 ,7 ,6 ,8(第2次比较:3和7比较,3 < 7,不交换位置)
- 2 ,3 ,6 ,7 ,8(第3次比较:7和6比较,7 > 6,交换位置)
- 2 ,3 ,6 ,7 ,8(第4次比较:786比较,7 < 8,不交换位置)
经过第1次循环,此时剩下参与比较的数据:2 ,3 ,6 ,7
第2次循环:(原始数据个数 - 1次循环 = 现有数据个数,4个数据,比较3次)
- 2 ,3 ,6 ,7(第1次比较:2和3比较,2 < 3,不交换位置)
- 2 ,3 ,6 ,7(第2次比较:3和6比较,3 < 6,不交换位置)
- 2 ,3 ,6 ,7(第3次比较:6和7比较,6 < 7,不交换位置)
经过第2次循环,此时剩下参与比较的数据:2 ,3 ,6
第3次循环:(原始数据个数 - 2次循环 = 现有数据个数,3个数据,比较2次)
- 2 ,3 ,6(第1次比较:2和3比较,2 < 3,不交换位置)
- 2 ,3 ,6(第2次比较:3和6比较,3 < 6,不交换位置)
经过第3次循环,此时剩下参与比较的数据:2 ,3
第4次循环:(原始数据个数 - 3次循环 = 现有数据个数,2个数据,比较1次)
- 2 ,3 (第1次比较:2和3比较,2 < 3,不交换位置)
下面是使用Java代码实现冒泡排序算法的示例:
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[] arr = {5, 2, 8, 12, 1, 6, 3, 9};
System.out.println("排序前的数组:");
for (int num : arr) {
System.out.print(num + " ");
}
bubbleSort(arr);
System.out.println("\n排序后的数组:");
for (int num : arr) {
System.out.print(num + " ");
}
}
}
2、案例演示
package com.javase.数组.排序算法;
public class BubbleSort {
public static void bubbleSort(int[] arr){
int number_of_comparisons = 0;
int number_of_exchanges = 0;
//控制外层循环次数,为原始数据个数 - 1
for (int i = arr.length; i > 1; i--) {
//控制比较次数,为比较数据个数 - 1
for (int j = 0; j < i - 1; j++) {
number_of_comparisons++;
if(arr[j] > arr[j + 1]){
number_of_exchanges++;
//交换位置 :将arr[j]和arr[j + 1]交换位置
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
System.out.println("比较次数为:" + number_of_comparisons + ",交换次数为:" + number_of_exchanges);
}
public static void main(String[] args) {
/*int a = 100;
a = a++;
System.out.println(a);
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
System.out.println("============================");
for (int i = 0; i < 10; ++i) {
System.out.println(i);
}*/
// int[] ints = {9,8,10,7,6,0,11};
int[] ints = {3, 1, 6, 2, 5,92,43,5,23,87,54,3,8,90,33};
System.out.print("冒泡排序前的数据顺序: ");
for (int i = 0; i < ints.length; i++) {
System.out.print(ints[i] + " ");
}
System.out.println();
//进行冒泡排序
bubbleSort(ints);
System.out.print("冒泡排序后的数据顺序: ");
for (int i = 0; i < ints.length; i++) {
System.out.print(ints[i] + " ");
}
}
}
八、选择排序(排序算法)
选择排序(Selection Sort)是一种简单的排序算法,它每次从待排序的元素中选择最小(或最大)的元素,然后将其放置在已排序序列的末尾。通过重复这个过程,直到所有元素都被排序,最终实现整个列表的排序。
1、选择排序比冒泡排序的效率高,高在交换位置的次数上。选择排序的交换位置是有意义的。(冒泡排序和选择排序比较的次数相同,选择排序的交换的次数少)
2、每一次循环,都找出“参加比较的这堆数据”中最小的,拿着这个最小的数据和“参加比较的这堆数据”中最前面的数据交换位置
以下是选择排序算法的详细步骤:
-
首先,从待排序的列表中选择第一个元素作为当前最小(或最大)元素。
-
遍历剩余的未排序元素,将每个元素与当前最小(或最大)元素进行比较。
-
如果找到比当前最小(或最大)元素更小(或更大)的元素,则更新当前最小(或最大)元素。
-
完成一轮遍历后,将当前最小(或最大)元素与第一个未排序元素交换位置,将最小(或最大)元素放置在已排序序列的末尾。
-
重复执行步骤2至步骤4,直到所有元素都被排序。
1、原理分析
原始数据:3 ,1 ,6 ,2 ,5 原始数据个数为5
5个数据,循环4次
第1次循环结果:(参与数据:3 ,1 ,6 ,2 ,5 )
- 1 ,3 ,6 ,2 ,5 (最左边元素索引为0)
第2次循环结果:(参与数据:3 ,6 ,2 ,5)
- 2 ,6 ,3 ,5(最左边元素索引为1)
第3次循环结果:(参与数据:6 ,3 ,5)
- 3 ,6 ,5 (最左边元素索引为2)
第4次循环结果:(参与数据:6 ,5)
- 6 ,5(最左边元素索引为3)
2、案例演示
一般假设参与比较的数据的最左侧的数据是最小的,用最小的这个数据和其他位置处的数据依次进行比较,若还有比假设的最小值还小的,则将参与比较的数据的最左侧元素(假设最小元素)与目前发现的更小元素进行位置交换
package com.javase.数组.排序算法;
public class SelectSort {
public static void selectSort(int[] ints) {
int number_of_comparisons = 0;
int number_of_exchanges = 0;
/**
* 控制外层循环次数
* 正好是“参加比较的这堆数据中”最左边那个元素的索引
* 一般假设参与比较的数据的最左侧的数据是最小的,用最小的这个数据和其他位置处的数据依次进行比较,
* 若还有比假设的最小值还小的,则将参与比较的数据的最左侧元素(假设最小元素)与目前发现的更小元素进行位置交换
*
* 所以i可以是参与比较的这堆数据的起点索引
*/
for (int i = 0; i < ints.length - 1; i++) {
// System.out.println(i);
//假设min_index为参与比较数据中的最小数据的索引,在参与比较数据的最左侧
int min_index = i;
//控制比较次数
for (int j = i + 1; j < ints.length; j++) {
// System.out.print(j + " ");
number_of_comparisons++;
if(ints[j] < ints[min_index]){
min_index = j;
}
}
// System.out.println();
//当i和min相等是,表示最初猜测是对的
//当i和min不相等时,表示最初猜测是错的,有比min索引处元素更小的元素
//需要拿着这个更小的元素和最左边的元素交换位置
if(min_index != i){
number_of_exchanges++;
//表示存在更小的数据
//ints[i] 假定的最小的数据,在最左侧
//ints[min] 实际更小的数据
int temp = ints[min_index];
ints[min_index] = ints[i];
ints[i] = temp;
}
}
System.out.println("比较次数为:" + number_of_comparisons + ",交换次数为:" + number_of_exchanges);
}
public static void main(String[] args) {
// int[] ints = {3, 1, 6, 2, 5};
int[] ints = {3, 1, 6, 2, 5,92,43,5,23,87,54,3,8,90,33};
System.out.print("选择排序前的数据顺序: ");
for (int i = 0; i < ints.length; i++) {
System.out.print(ints[i] + " ");
}
System.out.println();
//进行选择排序
selectSort(ints);
System.out.print("选择排序后的数据顺序: ");
for (int i = 0; i < ints.length; i++) {
System.out.print(ints[i] + " ");
}
}
}
九、二分法(查找元素算法)
二分法查找(Binary Search),也称为折半查找,是一种高效的查找算法,用于在有序数组或列表中查找特定元素的位置。它的基本思想是通过将待查找区间不断缩小一半,直到找到目标元素或确定目标元素不存在。
二分法查找算法是基于排序的基础之上。(没有排序的数据是无法查找的)
以下是二分法查找算法的详细步骤:
-
确定待查找的有序数组(或列表)和目标元素。
-
初始化两个指针:
low
指向数组的起始位置,high
指向数组的末尾位置。 -
进入循环,直到
low
大于high
:
- 计算中间位置
mid
,可以使用(low + high) / 2
或low + (high - low) / 2
。 - 比较中间位置的元素与目标元素的大小:
- 如果中间元素等于目标元素,则找到目标元素,返回中间位置。
- 如果中间元素大于目标元素,则目标元素可能在左半部分,将
high
更新为mid - 1
。 - 如果中间元素小于目标元素,则目标元素可能在右半部分,将
low
更新为mid + 1
。
4. 如果循环结束后仍未找到目标元素,则表示目标元素不存在于数组中。
二分法查找的终止条件:
情况一:能找到元素,一直折半,直到中间的那个元素恰好是被查找的元素,终止
情况二:找不到元素,low > high时,表示目标元素不存在,终止
1、不使用二分法查找元素
数组查找元素有两种方式:
第一种方式:一个一个挨着找,直到找到为止
第二种方式:二分法查找(算法),效率较高
package com.javase.数组.查找元素算法;
/**
* 演示不使用二分法查找元素
*/
public class ArraySearch {
/**
* 从数组中检索某个元素的索引(返回的是第一个元素的索引)
* @param ints 被检索的数组
* @param target 被检索的元素
* @return 大于等于0的数,表示索引,返回-1,表示该元素不存在
*/
public static int searchElement(int[] ints,int target){
for (int i = 0; i < ints.length; i++) {
if(ints[i] == target){
return i;
}
}
return -1;
}
public static void main(String[] args) {
int[] ints = {4,5,6,87,8};
//需求:找出87 的索引,如果没有返回 -1
int index = searchElement(ints,5);
System.out.println(index == -1?"该元素不存在" : "该元素索引为:" + index);
}
}
2、二分法原理
原始数据个数为奇数:
原始数据:10 ,11 ,12 ,13 ,14 ,15 ,16 ,17 ,18 ,19 ,20
索引: 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10
被查找元素为18
开始元素的索引为0,结束元素索引为10
第1次计算中间元素索引 mid = (0 + 10)/ 2 = 5
使用中间元素和目标要查找的元素进行比较:
中间元素:array[5] ---》 15
15 < 18(被查找的元素)
被查找的元素18在目前中间元素15的右边
开始元素的索引从0变成了5 + 1,结束元素索引为10
第2次计算中间元素索引 mid = (6 + 10)/ 2 = 8
使用中间元素和目标要查找的元素进行比较:
中间元素:array[8] ---》 18
18 == 18(被查找的元素)
找到的中间元素正好和被查找的元素18相等,表示找到了:索引为8
原始数据个数为偶数:
原始数据:10 ,11 ,12 ,13 ,14 ,15 ,16 ,17 ,18 ,19
索引: 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9
被查找元素为19
开始元素的索引为0,结束元素索引为9
第1次计算中间元素索引 mid = (0 + 9)/ 2 = 4
使用中间元素和目标要查找的元素进行比较:
中间元素:array[4] ---》 14
14 < 19(被查找的元素)
被查找的元素19在目前中间元素14的右边
开始元素的索引从0变成了4 + 1,结束元素索引为9
第2次计算中间元素索引 mid = (5 + 9)/ 2 = 7
使用中间元素和目标要查找的元素进行比较:
中间元素:array[7] ---》 17
17 < 19(被查找的元素)
被查找的元素19在目前中间元素17的右边
开始元素的索引从0变成了7 + 1,结束元素索引为9
第3次计算中间元素索引 mid = (8 + 9)/ 2 = 8
使用中间元素和目标要查找的元素进行比较:
中间元素:array[8] ---》 18
18 < 19(被查找的元素)
被查找的元素19在目前中间元素18的右边
开始元素的索引从0变成了8 + 1,结束元素索引为9
第4次计算中间元素索引 mid = (9 + 9)/ 2 = 9
使用中间元素和目标要查找的元素进行比较:
中间元素:array[9] ---》 19
19 == 19(被查找的元素)
找到的中间元素正好和被查找的元素19相等,表示找到了:索引为9
3、案例演示
package com.javase.数组.查找元素算法;
public class ArrayBinarySearch {
/**
* 从数组中查找目标元素的索引
* @param ints 被查找的数组(这个必须是已经排序的。)
* @param target 目标元素
* @return -1表示该元素不存在,其他表示返回该元素的索引
*/
public static int binarySearch(int[] ints,int target){
int begin = 0;
int end = ints.length - 1;
int mid;
while (begin <= end){
mid = (begin + end) / 2;
if(ints[mid] < target){
//表示目标元素在中间元素的右侧
begin = mid + 1;
} else if (ints[mid] > target) {
//表示目标元素在中间元素的左侧
end = mid - 1;
}else {
//表示目标元素就是中间元素
return mid;
}
}
return -1;
}
public static void main(String[] args) {
int[] ints = {100,200,230,235,600,1000,2000,9999};
int index = binarySearch(ints,9999);
System.out.println(index == -1? "目标元素不存在" : "目标元素索引为:" + index);
}
}
十、Arrays工具类
在Java中,java.util.Arrays
是一个工具类,提供了各种用于处理数组的静态方法。下面是Arrays
工具类的常见用法和详解:
1、排序:Arrays.sort(T[ ] a)
对数组进行升序排序。
2、查找:Arrays.binarySearch(T[ ] a, T key)
在使用Arrays.binarySearch()方法之前,必须先使用Arrays.sort()方法对数组进行排序
- 如果找到了指定的元素,则返回元素的索引值(非负数)。
- 如果没有找到指定的元素,则返回一个负数,表示应该插入元素以保持有序的位置。
元素不存在,返回负数的规则:
- 如果插入位置在数组的范围内(即
-1
到-n-1
,其中n
是数组的长度),则返回的值为-(insertion point) - 1
。- 如果插入位置超出了数组的范围(即
-n
),则返回的值为-n - 1
。
package com.javase.数组.Arrays工具类;
import java.util.Arrays;
public class ArraysTest01 {
public static void main(String[] args) {
int[] ints = {3, 6, 5, 12, 1, 2, 32, 5, 5};
//排序
Arrays.sort(ints);
//输出
for (int i = 0; i < ints.length; i++) {
System.out.print(ints[i] + " ");
}
//二分法查找(建立在排序基础之上)
int index = Arrays.binarySearch(ints, 12);
System.out.println(index == -1 ? "该元素不存在" : "该元素索引为:" + index);
}
}
3、Arrays.toString( ) 静态方法。它用于将数组转换为字符串形式,便于输出和打印数组的内容。
Arrays.toString()
是Java中Arrays
类的一个静态方法。它用于将数组转换为字符串形式,便于输出和打印数组的内容。
方法签名为:
public static String toString(Object[] array)
返回类型为 String
,表示包含数组内容的字符串。
下面是 Arrays.toString()
方法的详细解释:
-
toString()
方法接受一个数组作为参数,并将该数组的内容转换为字符串。 -
转换后的字符串将以以下形式呈现:
[element1, element2, ..., elementN]
,其中element1, element2, ..., elementN
是数组中的元素。 -
如果数组是多维的,则将数组展平为一维,并按照上述格式输出。
-
如果数组中的元素是基本类型(如
int[]
),它们将被转换为相应的字符串表示。 -
如果数组中的元素是对象类型(如
String[]
),它们将调用各自的toString()
方法来获取字符串表示。
下面是一个使用 Arrays.toString()
方法的示例:
int[] numbers = {1, 2, 3, 4, 5};
String[] names = {"John", "Jane", "Alice"};
System.out.println(Arrays.toString(numbers)); // 输出 "[1, 2, 3, 4, 5]"
System.out.println(Arrays.toString(names)); // 输出 "[John, Jane, Alice]"
在上述示例中,分别使用 Arrays.toString()
方法打印了整型数组 numbers
和字符串数组 names
的内容。
需要注意的是,Arrays.toString()
方法仅适用于数组类型,而不适用于其他集合类型(如 List
、Set
等)。对于其他集合类型,可以使用相应的转换方法或循环来打印集合的内容。
4、Array.copyOf() 浅拷贝
用于创建一个新的数组,并将原始数组的内容复制到新数组中
Arrays.copyOf()
方法是Java中Arrays
类提供的一个用于复制数组的方法。它用于创建一个新的数组,并将原始数组的内容复制到新数组中。
默认实现:
public static int[] copyOf(int[] original, int newLength) {
int[] copy = new int[newLength];
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
底层调用System.arraycopy()方法
copyOf()
方法有两个参数:
original
:原始数组,需要被复制的数组。newLength
:新数组的长度。
方法的返回值是一个新的数组,其中包含了原始数组的内容。如果新数组的长度大于原始数组的长度,那么多出来的部分会用默认值填充(对于引用类型是 null
,对于数值类型是 0
);如果新数组的长度小于原始数组的长度,那么只会复制原始数组中对应长度的部分。
以下是 Arrays.copyOf()
方法的示例用法:
int[] numbers = {1, 2, 3, 4, 5};
int[] copiedNumbers = Arrays.copyOf(numbers, 7);
System.out.println(Arrays.toString(copiedNumbers)); //[1, 2, 3, 4, 5, 0, 0]
在上述示例中,原始数组 numbers
的长度是5,然后使用 Arrays.copyOf()
方法将其复制到一个新的数组 copiedNumbers
中,新数组的长度设置为7。由于新数组长度大于原始数组,所以多出来的两个位置会用默认值 0
填充。
需要注意的是,Arrays.copyOf()
方法是浅拷贝,即对于引用类型的数组,只会复制引用而不会复制对象本身。如果需要深度复制数组,需要使用其他方法或手动进行遍历复制。
此外,还有一个重载的 Arrays.copyOf()
方法可以用于复制指定范围的数组元素。它接受三个参数:original
原始数组,newLength
新数组的长度,newType
新数组的类型。该方法可以用于改变数组类型或复制部分数组内容。
5、遍历数组的2种方式
package com.javase.collection.forEach增强for循环;
/**
* JDK5.0之后推出了一个新特性,叫做增强for循环,或者叫forEach
*/
public class ForEachTest01<T> {
public static void main(String[] args) {
ForEachTest01<Integer> f = new ForEachTest01<>();
Integer[] ints = {1, 2, 3, 4, 5};
//遍历数组:
//第一种方式:
// for (int i = 0; i < ints.length; i++) {
// System.out.println(ints[i]);
// }
f.demo1(ints);
System.out.println("==============================");
//第二种方式
// for (int element : ints) {
// System.out.println(element);
// }
f.demo2(ints);
System.out.println("==============================");
}
public void demo1(T[] array) {
for (int i = 0; i < array.length; i++) {
System.out.println(array[i]);
}
}
public void demo2(T[] array){
for (T element : array) {
System.out.println(element);
}
}
}