java八股---java02(面向对象、类、变量、方法、值传递)
面向对象
面向对象概述
面向对象和面向过程的区别
面向过程:
优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、 Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
缺点:没有面向对象易维护、易复用、易扩展面向对象:
优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加 灵活、更加易于维护
缺点:性能比面向过程低面向过程是具体化的,流程化的,解决一个问题,你需要一步一步的分析,一步一步的实现。
面向对象是模型化的,你只需抽象出一个类,这是一个封闭的盒子,在这里你拥有数据也拥有解决问题的方法。需要什 么功能直接使用就可以了,不必去一步一步的实现,至于这个功能是如何实现的,管我们什么事?我们会用就可以了。
面向对象的底层其实还是面向过程,把面向过程抽象成类,然后封装,方便我们使用的就是面向对象了
面向对象三大特性
面向对象的特征有哪些方面
面向对象的特征主要有以下几个方面:
抽象:
抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些 属性和行为,并不关注这些行为的细节是什么。
封装 :
封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,我们大可不必 提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。
继承:
继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功 能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。
关于继承如下 3 点请记住:
- 1. 子类拥有父类非 private 的属性和方法。
- 2. 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
- 3. 子类可以用自己的方式实现父类的方法。
多态
所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在 程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现 的方法,必须在由程序运行期间才能决定。
在Java中有两种形式可以实现多态:
继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。
其中Java 面向对象编程三大特性:
封装:隐藏对象的属性和实现细节,仅对外提供公共访问方式,将变化隔离,便于使用,提高复用性和安全性。
继承:继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承可以提高代码复用性。继承是多态的前提。
多态性:父类或接口定义的引用变量可以指向子类或具体实现类的实例对象。提高了程序的拓展性。
在Java中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。
方法重载(overload)实现的是编译时的多态性(也称为前绑定)
方法重写(override)实现的是运行时的多态性 (也称为后绑定)。一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。运行时的多态是面向对象最精髓的东西,要实现多态需要做两件事:
1、方法重写(子类继承父类并重写父类中已有的或抽象的方法);
2、对象造型(用父类型引用子类型对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为)。
什么是多态机制?Java语言是如何实现多态的?
所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在 程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现 的方法,必须在由程序运行期间才能决定。
因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让 引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运 行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。
多态分为编译时多态和运行时多态。其中编辑时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分 不同的函数,通过编辑之后会变成两个不同的函数,在运行时谈不上多态。而运行时多态是动态的,它是通过动态绑定 来实现的,也就是我们所说的多态性。
多态的实现
Java实现多态有三个必要条件:继承、重写、向上转型。
继承:在多态中必须存在有继承关系的子类和父类。
重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法
向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。
只有满足了上述三个条件,我们才能够在同一个继承结构中使用统一的逻辑实现代码处理不同的对象,从而达到执行不 同的行为。
对于Java而言,它多态的实现机制遵循一个原则:当超类对象引用变量引用子类对象时,被引用对象的类型而不是引用 变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法。
面向对象五大基本原则是什么(可选)
- 单一职责原则SRP(Single Responsibility Principle) 类的 功能要单一,不能包罗万象,跟杂货铺似的。
- 开放封闭原则OCP(Open-Close Principle) 一个模块对于拓展是开放的,对于修改是封闭的,想要增加功能热烈欢迎,想要修改,哼,一万个不乐意。
- 里式替换原则LSP(the Liskov Substitution Principle LSP) 子类可以替换父类出现在父类能够出现的任何地方。比如你能代表你爸去你姥姥家干活。哈哈~~
- 依赖倒置原则DIP(the Dependency Inversion Principle DIP) 高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。抽象不应该依赖于具体实现,具体实现应该依赖于抽象。就是你出国 要说你是中国人,而不能说你是哪个村子的。比如说中国人是抽象的,下面有具体的xx省,xx市,xx县。你要依赖的抽象是中国人,而不 是你是xx村的。
- 接口分离原则ISP(the Interface Segregation Principle ISP) 设计时采用多个与特定客户类有关的接口比采用一个通用的接口要好。就比如一个手机拥有打电话,看视频,玩游戏等功能,把这几个功 能拆分成不同的接口,比在一个接口里要好的多。
类与接口
抽象类和接口的对比
抽象类是用来捕捉子类的通用特性的。接口是抽象方法的集合。
从设计层面来说,抽象类是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。相同点
接口和抽象类都不能实例化
都位于继承的顶端,用于被其他实现或继承
都包含抽象方法,其子类都必须覆写这些抽象方法不同点
备注:Java8中接口中引入默认方法和静态方法,以此来减少抽象类和接口之间的差异。 现在,我们可以为接口提供默认实现的方法了,并且不用强制子类来实现它。
接口和抽象类各有优缺点,在接口和抽象类的选择上,必须遵守这样一个原则:
- 行为模型应该总是通过接口而不是抽象类定义,所以通常是优先选用接口,尽量少用抽象类。
- 选择抽象类的时候通常是如下情况:需要定义子类的行为,又要为子类提供通用的功能。
普通类和抽象类有哪些区别?
- 普通类不能包含抽象方法,抽象类可以包含抽象方法。
- 抽象类不能直接实例化,普通类可以直接实例化。
抽象类能使用 final 修饰吗?
不能,定义抽象类就是让其他类继承的,如果定义为 final 该类就不能被继承,这样彼此就会产生矛盾,所以 final 不能 修饰抽象类
创建一个对象用什么关键字?对象实例与对象引用有何不同?
new关键字,new创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。
一个 对象引用可以指向0个或1个对象(一根绳子可以不系气球,也可以系一个气球);一个对象可以有n个引用指向它(可以 用n条绳子系住一个气球)
变量与方法
成员变量与局部变量的区别有哪些
变量:在程序执行的过程中,在某个范围内其值可以发生改变的量。
从本质上讲,变量其实是内存中的一小块区域
成员变量:方法外部,类内部定义的变量
局部变量:类的方法中的变量。
成员变量和局部变量的区别
- 作用域
成员变量:针对整个类有效。
局部变量:只在某个范围内有效。(一般指的就是方法,语句体内)- 存储位置
成员变量:随着对象的创建而存在,随着对象的消失而消失,存储在堆内存中。
局部变量:在方法被调用,或者语句被执行的时候存在,存储在栈内存中。当方法调用完,或者语句结束后,就自动释放。- 生命周期
成员变量:随着对象的创建而存在,随着对象的消失而消失
局部变量:当方法调用完,或者语句结束后,就自动释放。- 初始值
成员变量:有默认初始值。
局部变量:没有默认初始值,使用前必须赋值。- 使用原则
在使用变量时需要遵循的原则为:就近原则
首先在局部范围找,有就使用;接着在成员位置找。
在Java中定义一个不做事且没有参数的构造方法的作用
Java程序在执行子类的构造方法之前,如果没有用super()来调用父类特定的构造方法,则会调用父类中“没有参数的构 造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用super()来调用父类中特定的 构造方法,则编译时将发生错误,因为Java程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加 上一个不做事且没有参数的构造方法。
在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是?
帮助子类做初始化工作。
一个类的构造方法的作用是什么?若一个类没有声明构造方法,改程序能正确执行吗?为什么?
主要作用是完成对类对象的初始化工作。可以执行。因为一个类即使没有声明构造方法也会有默认的不带参数的构造方 法。
构造方法有哪些特性?
名字与类名相同;
没有返回值,但不能用void声明构造函数;
生成类的对象时自动执行,无需调用。
静态变量和实例变量区别
- 静态变量: 静态变量由于不属于任何实例对象,属于类的,所以在内存中只会有一份,在类的加载过程中,JVM只为静态变量分配一次内存空间。
- 实例变量: 每次创建对象,都会为每个对象分配成员变量内存空间,实例变量是属于实例对象的,在内存中,创建几次对象,就有几份成员变量。
静态变量与普通变量区别
static变量也称作静态变量。
静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本, 它当且仅当在类初次加载时会被初始化。
而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本, 各个对象拥有的副本互不影响。还有一点就是static成员变量的初始化顺序按照定义的顺序进行初始化。
静态方法和实例方法有何不同?
静态方法和实例方法的区别主要体现在两个方面:
- 1. 在外部调用静态方法时,可以使用"类名.方法名"的方式,也可以使用"对象名.方法名"的方式。而实例方法只有 后面这种方式。也就是说,调用静态方法可以无需创建对象。
- 2. 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员 变量和实例方法;实例方法则无此限制
在一个静态方法内调用一个非静态成员为什么是非法的?
由于静态方法可以不通过对象进行调用,因此在静态方法里,不能调用其他非静态变量,也不可以访问非静态变量成 员。
什么是方法的返回值?返回值的作用是什么?
方法的返回值是指我们获取到的某个方法体中的代码执行后产生的结果!(前提是该方法可能产生结果)。返回值的作 用:接收出结果,使得它可以用于其他的操作!
内部类
什么是内部类?
在Java中,可以将一个类的定义放在另外一个类的定义内部,这就是内部类。内部类本身就是类的一个属性,与其他属 性定义方式一致。
内部类的分类有哪些
内部类可以分为四种:成员内部类、局部内部类、匿名内部类和静态内部类。
静态内部类
定义在类内部的静态类,就是静态内部类。
静态内部类可以访问外部类所有的静态变量,而不可访问外部类的非静态变量;
静态内部类的创建方式,new 外部类.静态 内部类()成员内部类
定义在类内部,成员位置上的非静态类,就是成员内部类。
成员内部类可以访问外部类所有的变量和方法,包括静态和非静态,私有和公有。成员内部类依赖于外部类的实例,
它的创建方式外部类实例.new 内部类()局部内部类
定义在方法中的内部类,就是局部内部类。
定义在实例方法中的局部类可以访问外部类的所有变量和方法,定义在静态方法中的局部类只能访问外部类的静态变量和方法。
局部内部类的创建方式,在对应方法内,new 内部类()匿名内部类
匿名内部类就是没有名字的内部类,日常开发中使用的比较多。
除了没有名字,匿名内部类还有以下特点:
- 匿名内部类必须继承一个抽象类或者实现一个接口。
- 匿名内部类不能定义任何静态成员和静态方法。
- 当所在的方法的形参需要被匿名内部类使用时,必须声明为 final。
- 匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法。
内部类的优点我们为什么要使用内部类呢?因为它有以下优点:
- 一个内部类对象可以访问创建它的外部类对象的内容,包括私有数据!
- 内部类不为同一包的其他类所见,具有很好的封装性;
- 内部类有效实现了“多重继承”,优化 java 单继承的缺陷。
- 匿名内部类可以很方便的定义回调。
内部类有哪些应用场景
1. 一些多算法场合
2. 解决一些非面向对象的语句块。
3. 适当使用内部类,使得代码更加灵活和富有扩展性。
4. 当某个类除了它的外部类,不再被其他的类使用时。
局部内部类和匿名内部类访问局部变量的时候,为什么变量必须要加上final?
局部内部类和匿名内部类访问局部变量的时候,为什么变量必须要加上final呢?它内部原理是什么呢?
public class Outer{void outMethod(){final int a =10;class Inner{void innerMethod(){System.out.println(a);}}} }
以上例子,为什么要加final呢?
是因为生命周期不一致, 局部变量直接存储在栈中,当方法执行结束后,非final的局部 变量就被销毁。
而局部内部类对局部变量的引用依然存在,如果局部内部类要调用局部变量时,就会出错。加了final, 可以确保局部内部类使用的变量与外层的局部变量区分开,解决了这个问题。
重写与重载
构造器(constructor)是否可被重写(override)
构造器的作用:构造器用于初始化对象,其名称必须与类名一致。每个类可以有多个构造器(通过重载),但不能通过重写改变其行为。
继承中的构造器:子类不能重写父类的构造器。子类的构造器可以通过
super()
调用父类构造器,但这不属于重写。方法重写的定义:重写是指子类重新定义父类中已有的方法,要求方法名、参数列表和返回类型相同。构造器因名称必须与类名一致,无法满足这一条件。
实例化过程:创建子类对象时,会先调用父类构造器,再调用子类构造器。如果允许重写,会导致对象初始化不一致。
重载(Overload)和重写(Override)的区别。重载的方法能否根据返回类型进行区分?
定义:
重载(Overload):在同一个类中,方法名相同但参数列表不同(参数类型、数量或顺序不同)。重载与返回类型无关。
重写(Override):在子类中重新定义父类中已有的方法,方法名、参数列表和返回类型必须完全相同。
作用范围:
重载发生在同一个类中。
重写发生在父子类之间。
访问修饰符:
重载对访问修饰符没有限制。
重写的方法访问修饰符不能比父类方法更严格(例如,父类方法是
protected
,子类方法可以是public
,但不能是private
)。异常处理:
重载对异常没有特殊要求。
重写的方法不能抛出比父类方法更宽泛的检查异常。
绑定方式:
重载是编译时多态(静态绑定)。
重写是运行时多态(动态绑定)。
重载的方法能否根据返回类型进行区分?
不能。重载仅根据方法的参数列表(参数类型、数量或顺序)来区分,与返回类型无关。如果两个方法只有返回类型不同,编译器会报错。
对象相等判断
== 和 equals 的区别是什么
== : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。
(基本数据类型 == 比较的是 值,引用数据类型 == 比较的是内存地址)equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:
情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来两个对象的内容相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。
对象的相等与指向他们的引用相等,两者有什么不同?
对象的相等 比的是内存中存放的内容是否相等而
引用相等 比较的是他们指向的内存地址是否相等
hashCode 与 equals (重要)
HashSet如何检查重复
hashCode() 的作用: 用于快速定位元素在哈希表中的位置。 如果两个对象的 hashCode() 不同,HashSet 会认为它们是不同的对象,无需调用 equals()。
equals() 的作用: 用于精确比较对象的内容。 如果两个对象的 hashCode() 相同,HashSet 会调用 equals() 进一步确认是否重复。
哈希冲突的处理: 当多个对象的 hashCode() 相同时,HashSet 会将它们存储在同一个桶中(通过链表或红黑树)。 在查找或添加元素时,HashSet 会遍历桶中的所有元素,调用 equals() 进行比较。
HashSet 检查重复的流程:
调用 hashCode() 计算哈希码,确定存储位置。
如果发生哈希冲突,调用 equals() 比较对象内容。
如果 equals() 返回 true,则认为元素重复,拒绝添加。
因此,HashSet 的高效性依赖于 hashCode() 和 equals() 的正确实现。
两个对象的 hashCode() 相同,则 equals() 也一定为 true,对吗?
hashCode() 和 equals() 的关系
hashCode()
:返回对象的哈希码,是一个整数。哈希码用于快速定位对象(例如在哈希表中)。
equals()
:用于比较两个对象的内容是否相等。根据 Java 规范:
如果两个对象的
equals()
为true
,则它们的hashCode()
必须相同。但如果两个对象的
hashCode()
相同,它们的equals()
不一定为true
。为什么 hashCode() 相同,equals() 不一定为 true?
哈希码是通过哈希函数计算得出的,而哈希函数可能会将不同的输入映射到相同的输出(称为哈希冲突)。因此,即使两个对象的哈希码相同,它们的内容也可能不同。
为什么重写 equals()
时必须重写 hashCode()
方法?
在 Java 中,equals() 和 hashCode() 方法密切相关,它们的正确实现是保证对象在哈希表(如 HashMap、HashSet 等)中正常工作的基础。以下是必须同时重写 equals() 和 hashCode() 的原因:
1、哈希表的工作原理
哈希表(如 HashMap、HashSet)依赖于 hashCode() 和 equals() 方法来存储和查找对象: hashCode():用于快速定位对象在哈希表中的存储位置(桶)。
equals():用于精确比较对象的内容,解决哈希冲突。
如果只重写 equals() 而不重写 hashCode(),可能会导致以下问题: 两个对象在逻辑上相等(equals() 返回 true),但它们的 hashCode() 不同。 这会导致哈希表无法正确识别重复对象,破坏哈希表的正常行为。2、Java 规范的要求
根据 Java 规范:
如果两个对象的 equals() 返回 true,则它们的 hashCode() 必须相同。如果两个对象的 equals() 返回 false,它们的 hashCode() 可以相同(哈希冲突),也可以不同。
如果只重写 equals() 而不重写 hashCode(),可能会违反这一规范,导致程序行为异常。
如何正确重写 hashCode() 和 equals()
equals() 的重写规则
- 自反性:x.equals(x) 必须为 true。
- 对称性:如果 x.equals(y) 为 true,则 y.equals(x) 也必须为 true。
- 传递性:如果 x.equals(y) 为 true,且 y.equals(z) 为 true,则 x.equals(z) 必须为 true。
- 一致性:多次调用 equals() 的结果必须一致。
- 非空性:x.equals(null) 必须为 false。
hashCode() 的重写规则
- 如果两个对象的 equals() 为 true,则它们的 hashCode() 必须相同。
- 如果两个对象的 equals() 为 false,它们的 hashCode() 可以相同,也可以不同。
- hashCode() 的计算应尽量均匀分布,以减少哈希冲突。
值传递
当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结 果,那么这里到底是值传递还是引用传递
是值传递。Java 语言的方法调用只支持参数的值传递。当一个对象实例作为一个参数被传递到方法中时,参数的值就是 对该对象的引用。对象的属性可以在被调用过程中被改变,但对对象引用的改变是不会影响到调用者的
Java 中的值传递
当将一个对象作为参数传递给方法时,实际上传递的是该对象的引用的副本(即对象在堆内存中的地址值的副本)。
方法内部操作的是这个引用的副本,而不是原始引用本身。
对象属性的修改
由于方法接收的是对象引用的副本,而这个副本仍然指向堆内存中的原始对象,因此方法可以通过这个副本修改对象的属性。
这种修改会反映到原始对象上,因为方法操作的是堆内存中的同一对象。
为什么 Java 中只有值传递
在 Java 中,确实只有值传递。这个说法是因为在 Java 中传递的是参数的副本,而不是直接传递原始数据本身或引用。
Java 中的传递有两种情况:
- 基本类型:传递的是该基本类型变量的副本,修改副本不会影响原始变量的值。
- 对象类型(引用类型):传递的是对象引用的副本,也就是说传递的是对象引用的副本,而不是对象本身。通过这个副本可以访问和修改对象的状态,但不能改变引用指向的对象。
对于基本数据类型(例如
int
、float
、char
等),Java 传递的是变量的值,即将实际的值复制给参数。修改参数的值不会影响原始值。对于引用类型(例如对象、数组等),Java 传递的是对象引用的副本。虽然可以通过该副本修改对象的状态(例如字段),但如果改变引用本身(让它指向其他对象),原始对象引用不会发生变化。
public class ReferencePassTest {public static void main(String[] args) {Person p = new Person("Alice");changeName(p); // 传递的是p对象的引用的副本System.out.println(p.name); // 输出 Bob,修改了对象的状态}public static void changeName(Person p) {p.name = "Bob"; // 修改对象的字段} }class Person {String name;Person(String name) {this.name = name;} }
在这个例子中,
changeName
方法接收的是p
引用的副本,因此它修改了p
指向的对象的状态(即name
字段)。但是,如果在方法中重新赋值p
,原始引用不会发生变化:public static void changeName(Person p) {p = new Person("Charlie"); // 只是改变了副本p的引用 }
这不会影响原始的
p
引用,它仍然指向最初的对象。总结Java 中的“值传递”意味着:
- 对于基本类型,传递的是变量值的副本。
- 对于引用类型,传递的是对象引用的副本,修改对象的状态会影响原始对象,但重新分配引用则不会影响原始引用。
值传递和引用传递有什么区别
值传递:指的是在方法调用时,传递的参数是按值的拷贝传递,传递的是值的拷贝,也就是说传递后就互不相关了。
引用传递:指的是在方法调用时,传递的参数是按引用进行传递,其实传递的引用的地址,也就是变量所对应的内存空间的地址。传递的是值的引用,也就是说传递前和传递后都指向同一个引用(也就是同一个内存空间)
java八股---java基础01-CSDN博客
java八股---java基础03(包、IO流、反射、String、包装类)-CSDN博客