谈谈对面向对象/面向过程的理解?
- 面向过程让计算机有步骤地顺序做一件事,是过程化思维。因为类调⽤时需要实例化,开销⽐较⼤,⽐较消耗资源,所以当性能是最重要的考量因素的时候,⽐如单⽚机、嵌⼊式开发Linux/Unix 等⼀般采⽤⾯向过程开发。但是,使用面向过程语言开发大型项目,软件复用和维护存在很大问题,模块之间耦合严重。
- 面向对象更适合解决规模较大的问题,可以拆解问题复杂度,对现实事物进行抽象并映射为开发对象,更接近人的思维。因为⾯向对象有封装、继承、多态性的特性,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护。
应用举例:
- 把大象放进冰箱里分为以下步骤:把冰箱门打开;把大象放进去;关上冰箱门(强调过程和过程中所涉及的行为/动作)。
- 用面向对象思想考虑:无论是打开冰箱,放进大象,关闭冰箱,所有操作都是操作冰箱这个对象,所以只需要将所有功能都定义在冰箱这个对象上,冰箱上就有打开、存储、关闭得所有功能 。
总结:
- 面向过程是一种过程化思维,强调流程化解决问题,性能更高,但是维护,复用较难,代码耦合性也较大;
- 面向对象代码强调高内聚、低耦合,先抽象模型定义共性行为,再解决实际问题,更接近人的思维,易维护、复用与扩展,但性能相对较低。ps:Java 性能差的主要原因并不是因为它是⾯向对象语⾔,⽽是 Java 是半编译语⾔,最终的执⾏代码并不是可以直接被 CPU 执⾏的⼆进制机器码。
类与对象的关系
面向对象的三大特性?
- 封装:封装就是把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,对数据的访问主要通过已经定义的接口,是一种信息隐藏技术;
- 封装的好处:(1)封装后可提供多处调用,减少耦合(2)隐藏数据信息,避免恶意修改带来的安全问题(3)类内部的结构可以自由更改而不会影响其他代码(4)能够对成员属性进行精准的控制
- 如何实现封装:(1)public (公有的):可以被该类的和非该类的任何成员访问。(2)private(私有的):仅仅可以被该类的成员访问,任何非该类的成员一概不能访问。(主要是隐藏数据来保证数据的安全性);如果需要访问可以调用它的内部类进行访问(3)protected(保护的):仅仅可以被子类和类本身还有同一个包里的类访问
- 继承:继承可以使用已经存在类的所有功能(父类所有非private属性和功能);子类可以拥有自己的属性和方法,即子类可以对父类进行扩展;子类可以用自己的方式实现父类的方法。
- 继承的优缺点:继承将所有子类公共的部分都放在父类,实现代码复用;但是,继承是一种父子类之间的强耦合关系,父类改变,子类不得不变,同时继承破坏了封装,将父类的实现细节暴露给子类。
- 继承特性(注意):(1)Java继承为单继承,具有传递性(2)⼦类拥有⽗类对象所有的属性和⽅法(包括私有属性和私有⽅法),但是⽗类中的私有属性和⽅法⼦类是⽆法访问,只是拥有。(3)⼦类可以拥有⾃⼰属性和⽅法,即⼦类可以对⽗类进⾏扩展(4)⼦类可以⽤⾃⼰的⽅式实现⽗类的⽅法。
- 多态:一个对象具有不同的形态,具体表现为:父类引用指向子类实例。
- 多态的特点:(1)对象类型和引用类型之间具有继承(类)/实现(接口)的关系;(2)引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定;(3)多态不能调用“只在子类存在但在父类不存在”的方法;(4)如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法。
- 注意:下面三种类型没法体现多态特性(不能被重写):(1)static修饰的方法属于类,不属于实例(2)final修饰的方法(3)private修饰的对子类不可以访问(4)protected修饰的方法可以被子类访问,也可以被重写,但是无法被外部引用(无多态)
- 多态的分类:编译时多态,方法的重载;运行时多态,方法的重写
ps:A a = New B(); 向上转型是JAVA中的一种调用方式,是多态的一种表现。向上转型并非是将B自动向上转型为A的对象,相反它是从另一种角度去理解向上两字的:它是对A的对象的方法的扩充,即A的对象可访问B从A中继承来的和B重写A的方法,其它的方法都不能访问,包括A中的私有成员方法。向上转型一定是安全的,没有问题的,正确的(主要是为了提高扩展性和维护性)。但是也有一个弊端:对象一旦向上转型为父类,那么就无法调用子类原本特有的内容。
访问修饰符
- private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类)
- default (即缺省,什么也不写,不使用任何关键字): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。
- protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)。
- public : 对所有类可见。使用对象:类、接口、变量、方法
修饰符 | 当前类 | 同包 | 子类 | 其他包 |
private | v | × | × | × |
default | v | v | × | × |
protected | v | v | v | × |
public | v | v | v | v |
重载与重写区别
重载:发生在同一个类中,方法名必须相同,参数列表(参数个数、类型不同,多个参数的顺序)必须不同,方法返回值和访问修饰符可以不同,发生在编译时(编译器会根据参数列表进行匹配,确定方法,如果失败,编译器报错,这叫做重载分辨)。
每个重构的方法(构造函数)都必须有独一无二的参数类型列表。最常见的地方就是构造器的重载,即构造函数。注意:构造方法不能被重写!
public class Test1 { public void out(){ System.out.println("参数"+null); } // 重载 ------ -----------方法名必须相同------------------------------- // 参数数目不同 public void out(Integer n){ System.out.println("参数"+n.getClass().getName()); } // 参数类型不同 public void out(String string){ System.out.println("参数"+string.getClass().getName()); } public void out(Integer n ,String string){ System.out.println("参数"+n.getClass().getName()+","+string.getClass().getName()); } //参数顺序不同 public void out(String string,Integer n){ System.out.println("参数"+string.getClass().getName()+","+n.getClass().getName()); } public static void main(String[] args) { Test1 test1 = new Test1(); test1.out(); test1.out(1); test1.out("string"); test1.out(1,"string"); test1.out("string",1); } }
重写:子类对父类中允许访问的方法进行重新编写(覆盖父类的方法),或者实现类对接口的重写,方法名和参数列表必须相同(可以理解为外壳不变)!
返回值范围和抛出异常范围小于等于父类,访问修饰符范围大于等于父类(子类方法的访问权限更大),发生在运行时;
便于记忆:两同(方法名和参数列表)两小(返回值和抛出异常的范围)一大(访问权限)!
注意:如果父类方法的访问修饰符为private,则子类不能重写该方法。
class Test{ public void out(){ System.out.println("我是父类方法"); } // 子类不能重写此方法 private void out1(){ System.out.println("我是父类方法"); } } public class Test1 extends Test{ @Override // 方法名与参数列表必须完全一致 public void out() { System.out.println("我是重写后的子类方法"); } public static void main(String[] args) { Test test = new Test(); test.out(); test = new Test1(); test.out(); } }
总结
重写(Overriding) | 重载(Overloading) | |
应用场景 | 父子类、接口与实现类 | 本类 |
方法名称 | 必须一致 | 必须一致 |
参数列表 | 一定不能修改(必须一致) | 必须修改(每个重构方法参数列表独一无二) |
返回类型 | 一定不能修改(必须一致) | 可以修改 |
异常 | 可以减少或删除,但不能扩展 | 可以修改 |
接口与抽象类的异同点与总结?
对于面向对象编程来说,抽象是它的一大特征之一。在Java中,可以通过两种形式来体现OOP的抽象:接口和抽象类。这两者有太多相似的地方,又有太多不同的地方。
两者的相同点:
- 都是Java中体现OOP中抽象的基本形式。
- 都不能被实例化,即不能进行new操作。因为:接口中没有构造方法,抽象类有构造方法,但是不是用来实例化的,是用来初始化的。
- 接口的实现类和抽象类的子类只有全部实现了接口或者抽象类中的方法后才可以被实例化。
主要区别:
- 实现接口的关键字为implements,继承抽象类的关键字为extends。抽象类只能单继承(Java),注意:一个子类继承一个抽象类,则子类必须实现父类抽象方法,否则子类也必须定义为抽象类;接口/类可以继承多个接口(接口支持多继承,解决了Java类单继承的局限);
- 包含抽象方法的一定是抽象类,但是抽象类不一定含有抽象方法。抽象类中的抽象方法的修饰符只能为public或者protected,默认为public;接口中的方法被隐式指定为public abstract(JDK1.8之前),但是JDK1.8中对接口增加了新的特性:(1)默认方法(default method):JDK 1.8允许给接口添加非抽象的方法实现,但必须使用default关键字修饰;定义了default的方法可以不被实现子类所实现,但只能被实现子类的对象调用;如果子类实现了多个接口,并且这些接口包含一样的默认方法,则子类必须重写默认方法;(2)静态方法(static method):JDK 1.8中允许使用static关键字修饰一个方法,并提供实现,称为接口静态方法。接口静态方法只能通过接口调用(接口名.静态方法名);
- 抽象类可以使用各种类型的成员变量,接口成员变量只能(默认)是常量型(public static final修饰),必须赋初值,不能被修改;
- 抽象类有构造方法,接口没有构造方法,接口只是对行为的抽象,没有具体的存在。在接口里写入构造方法时,编译器提示:Interfaces cannot have constructors;
解决的问题:
- 抽象类主要是为了代码复用,注意是先有子类后有父类,共性部分派生出一个抽象类。抽象类不允许实例化(抽象类中有些方法没实现,无法执行);
- 接口的设计目的是对类的行为有约束,即强制要求不同的类具有不同的行为。只约束有无,不对实现限制;
传递的信息(本质是否改变):
- 抽象类是对类本质的抽象,表达一种is a的关系(如奔驰是车,本质相同),抽象类包含并实现子类的通用特性,将子类的差异化特性进行抽象,交由子类去实现;
- 接口是对行为的抽象,表达一种like a的关系(飞机可以像鸟一样飞,本质不同),关心的是实现类可以做什么,至于主体和如何做并不关心。
应用场景:
- 抽象类:关注一个事务本质,设计抽象类代价高(编写出子类的所有共性,否则后期修改抽象类,需要修改所有子类);
- 接口:关注一个操作(能做什么,即实现的功能),接口设计(功能上)难度较低,可以在同一类中实现多个接口。
注意:抽象类不能用final修饰,定义抽象类就是让其他类继承的,如果定义为 final 该类就不能被继承,这样彼此就会产生矛盾。
补充:抽象方法
- 抽象方法还必须使用关键字abstract做修饰。
- 在所有的普通方法上面都会有一个“{}”,这个表示方法体,有方法体的方法一定可以被对象直接使用,抽象方法没有方法体。
- 抽象方法必须为public或者protected(因为如果为private,则不能被子类继承,子类便无法实现该方法),缺省情况下默认为public。
//没有方法体,有abstract关键字做修饰 public abstract void xxx();
抽象类是为了把相同的但不确定的东西的提取出来,为了以后的重用。定义成抽象类的目的,就是为了在子类中实现抽象方法。例如,构造一个动物类,包括“吃”,‘嚎叫’等方法。由于不同动物的 吃 和 嚎叫 是不同的,可以将 吃 和 嚎叫定义为抽象类。在继承这个动物类时,不同的动物再实现不同的 吃 和 嚎叫方法。
抽象类的使用注意点:
- 抽象类可以有构造方法。由于抽象类里会存在一些属性,那么抽象类中一定存在构造方法,其存在目的是为了属性的初始化。并且子类对象实例化的时候,依然满足先执行父类构造,再执行子类构造的顺序。
- 抽象类不能使用final声明,因为抽象类必须有子类,而final定义的类不能有子类。
- 抽象类能否使用static声明吗?外部抽象类不允许使用static声明,而内部的抽象类可以使用static声明。使用static声明的内部抽象类相当于一个外部抽象类,继承的时候使用“外部类.内部类”的形式表示类名称。
- 抽象类中的static方法可以直接调用。
hashCode与equals的区别和联系
hashCode概述
- hashCode()方法和equals()方法的作用其实一样,在Java里都是用来对比两个对象是否相等。
- hashCode() 的作用是获取哈希码(也称为散列码),它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,Java中的任何类都包含有hashCode() 函数。
- 哈希表(散列表)存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)。
- 另外需要注意的是: Object 的 hashcode ⽅法是本地⽅法(native),也就是⽤ c 语⾔或 c++ 实现的,该⽅法通常⽤来将对象的 内存地址 转换为整数之后返回。
为什么要有hashCode?
以“HashSet如何检查重复”为例子来说明为什么要hashCode:
- 对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,看该位置是否有值,如果没有,HashSet会假设对象没有重复出现,直接加入。
- 但是如果发现有值,这时会再调用equals()方法来检查两个对象是否真的相同。
- 如果两者相同,HashSet就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样就大大减少了equals的次数(避免新加入的对象与内部每个对象进行比较),相应就大大提高了执行速度。也就是说 hashcode 只是⽤来缩⼩查找成本。
什么时候重写hashCode?
- 一般的地方不需要,只有当类需要放在HashTable、HashMap、HashSet等hash结构的集合时才会重写hashCode。在散列表中hashCode() 的作用是获取对象的散列码,进而确定该对象在散列表中的位置。
重写equals,为什么还要重写hashCode呢?
- 如果只是重写了equals,比如说是基于对象的内容实现的,而保留hashCode的实现不变,那么很可能出现某两个对象明明是“相等”,而hashCode却不一样。这样,当你用其中的一个作为键保存到hashMap、hasoTable或hashSet中,再以“相等的”找另一个作为键值去查找他们的时候,则根本找不到。
- 如果两个对象相等,则 hashcode ⼀定也是相同的。两个对象相等,对两个对象分别调⽤ equals⽅法都返回 true。但是,两个对象有相同的 hashcode 值,它们也不⼀定是相等的 。因此,equals ⽅法被覆盖过,则 hashCode ⽅法也必须被覆盖。
ps:hashCode()的默认行为是对堆上的对象产生独特值(默认返回当前对象的地址)。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。
hashCode与equals区别:两者的区别主要体现在性能和可靠性。
- 性能:因为重写的equals()里一般比较复杂,这样效率就比较低;而利用hashCode()进行对比,则只要生成一个hash值进行比较就可以了,效率很高。
- 可靠性:
- equals()相等的两个对象他们的hashCode()肯定相等,也就是用equals()对比是绝对可靠的。
- hashCode()相等(哈希碰撞)的两个对象他们的equals()不一定相等,也就是hashCode()不是绝对可靠的。
补充:阿里开发规范
- 只要重写 equals,就必须重写 hashCode;
- 因为 Set 存储的是不重复的对象(上例),依据 hashCode 和 equals 进行判断,所以 Set 存储的对象必须重写这两个方法;
- 如果自定义对象做为 Map 的键,那么必须重写 hashCode 和 equals;
- String 重写了 hashCode 和 equals 方法,所以我们可以非常愉快地使用 String 对象作为 key 来使用;
super函数用法,与this能否同时使用?super和this关键字区别?
- 访问父类的构造函数:可以使用 super() 函数访问父类的构造函数,从而委托父类完成一些初始化的工作。应该注意到,子类一定会调用父类的构造函数来完成初始化工作,一般是调用父类的默认构造函数。
- 访问父类的成员:如果子类重写了父类的某个方法,可以通过使用 super 关键字来引用父类的方法实现。
super()和this()能不能同时使用?
不能同时使用,this和super不能同时出现在一个构造函数里面,因为this必然会调用其它的构造函数,其它的构造函数必然也会有super语句的存在,所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过。
this关键字:理解为本类的,先查找本类,如果本类没有再查找父类!
- 表示当前对象(区分成员变量与方法形参)【重要】
class People{ String name; public People(String name){//正确写法 this.name = name; } public People(String name){//错误写法 name = name;//当一个方法的形参与成员变量的名字相同时,就会覆盖成员变量 } }
- this调用构造器
①在我们类的构造器中,可以显示的使用“this(形参列表)”方式,调用本类中指定的其他构造器
②构造器中不能通过“this(形参列表)”方式调用自己
③如果一个类中有n个构造器,则最多有n-1构造器中使用了“this(形参列表)”
④规定:“this(形参列表)”必须声明在当前构造器的首行
⑤构造器内部,最多只能声明一个“this(形参列表)”,用来调用其他的构造器
super关键字:理解为父类的,不查找本类,直接调用父类的结构
常用情况:当子类重写了父类的方法后,我们想在子类方法中调用父类中被重写的方法时,必须显示的使用super.方法,表示调用的是父类中被重写的方法
- 调用父类被重写的方法
- super调用构造器
①我们可以在子类的构造器中显示的使用“super(形参列表)”的方式,调用父类中声明的制定的构造器
②“super(形参列表)”的使用,必须声明在子类构造器的首行
③我们在类的构造器中,针对于“this(形参列表)”或“super(形参列表)”只能二选一,不能同时出现
④在构造器的首行,没有显示的声明“this(形参列表)”或“super(形参列表)”,则默认调用的是父类中空参的构造器
⑤在类的多个构造器中,至少有一个类的构造器使用了“super(形参列表)”,调用父类中的构造器
成员变量与局部变量区别
变量:在程序执行的过程中,在某个范围内其值可以发生改变的量。从本质上讲,变量其实是内存中的一小块区域。
成员变量:方法外部,类内部定义的变量;局部变量:类的方法中的变量。区别如下:
- 作用域不同:
- 成员变量:针对整个类有效,属于类的。
- 局部变量:只在某个范围内有效。(一般指的就是方法,语句体内)
- 存储位置不同:
- 成员变量:存储在堆内存中。
- 局部变量:存储在栈中,如果局部变量类型为基本数据类型,那么存储在栈内存,如果为引⽤数据类型,那存放的是指向堆内存对象的引⽤或者是指向常量池中的地址。
- 生命周期不同:
- 成员变量:随着对象的创建而存在,随着对象的消失而消失。
- 局部变量:当方法调用完,或者语句结束后,就自动释放。
- 初始值不同:
- 成员变量:会⾃动以类型的默认值⽽赋值(⼀种情况例外:被 final 修饰的成员变量也必须显式地赋值)。
- 局部变量:没有默认初始值,使用前必须赋值。
- 修饰符不同:
- 成员变量:可以被 public , private , static ,final等修饰符所修饰,static修饰属于类,否则属于实例。
- 局部变量:不能被访问控制修饰符及 static 所修饰,但可以被final修饰
使用原则:就近原则,首先在局部范围找,有就使用;接着在成员位置找。
静态变量与实例变量的区别
两者主要区别在隶属对象、存储位置和存在个数:
- 隶属对象不同: 静态变量由于不属于任何实例对象,属于类的;每次创建对象,都会为每个对象分配成员变量内存空间,实例变量是属于实例对象的。
- 存储位置不同: 静态变量存储在方法区;实例变量存储在堆内存当中,其引用存在当前线程栈。
- 存在个数不同:静态变量内存中只会有一份,在类的加载过程中,JVM只为静态变量分配一次内存空间;而在内存中,创建几次对象,就有几份成员变量。
静态方法与实例方法的区别,为什么静态方法不能调用非静态成员?
两种方法主要区别在外部调用方式和访问本类成员的限制:
- 外部调用方式不同:在外部调用静态⽅法时,可以使用"类名.方法名"的方式,也可以使用"对象名.方法名"的方式;而实例方法只有使用"对象名.方法名"这种方式。也就是说,调⽤静态方法可以无需创建对象。
- 访问本类的成员限制:静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制。注意:静态方法中不能有 this 和 super 关键字,因此这两个关键字与具体对象关联。
由于静态方法可以不通过对象进行调用,静态方法属于类,不属于对象。静态资源在类初始化的过程加载的,非静态资源在类new的过程中加载的。因此静态方法里,不能调用和访问其他非静态成员。
Java内部类的作用是什么,有哪些分类?应用场景?
为什么需要内部类?
- 内部类方法可以访问该类定义所在作用域中的数据,包括被 private 修饰的私有数据
- 内部类可以对同一包中的其他类隐藏起来,非内部类是不可以使用 private和 protected修饰的,但是内部类却可以,从而达到隐藏的作用。同时也可以将一定逻辑关系的类组织在一起,增强可读性。
- 内部类间接实现 Java 多继承,
- 当我们想要定义一个回调函数却不想写大量代码的时候我们可以选择使用匿名内部类来实现
内部类分类:
- 静态内部类(嵌套类): 属于外部类,只加载一次。作用域仅在包内,可通过
外部类名.内部类名
直接访问,类内只能访问外部类所有静态属性和方法。HashMap 的 Node 节点,ReentrantLock 中的 Sync 类,ArrayList 的 SubList 都是静态内部类。内部类中还可以定义内部类,如 ThreadLoacl 静态内部类 ThreadLoaclMap 中定义了内部类 Entry。 - 非静态内部类
- 成员内部类: 属于外部类的每个对象,随对象一起加载。不可以定义静态成员和方法,可访问外部类的所有内容。
- 局部内部类: 定义在方法内,不能声明访问修饰符,只能定义实例成员变量和实例方法,作用范围仅在声明类的代码块中。
- 匿名内部类: 只用一次的没有名字的类,可以简化代码,创建的对象类型相当于 new 的类的子类类型。用于实现事件监听和其他回调。
静态内部类和非静态内部类的区别:
在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是? 子类的初始化顺序?
目的:帮助子类做初始化工作。子类初始化顺序:
- 父类的静态代码块和静态变量
- 子类静态代码块和静态变量
- 父类普通代码块和普通变量
- 父类构造方法
- 子类普通代码块和普通变量
- 子类构造方法
一个类的构造方法的作用是什么? 若一个类没有声明构造方法,该程序能正确执行吗? 构造方法有哪些特性?
构造方法主要作用:完成对类对象的初始化
没有声明类的构造方法,程序也可以执行,因为类有默认的不带参数的构造方法。
构造方法的特性:
- 名字与类名相同
- 没有返回值,但不能用void声明构造函数
- 生成类的对象时自动执行,无需调用
创建一个对象用什么运算符?对象实体与对象引用有何不同?
new 运算符:
- new创建的对象实例,存放在堆内存中
- 对象的引用(堆内存地址)指向对象实例,存放在栈内存中
对象引用:一个对象引用可以指向 0 个或 1 个对象(一根绳⼦可以不系⽓球,也可以系一个⽓球)
对象实体:一个对象可以有 n 个引用指向它(可以用 n条绳子系住一个气球)。
ps:对象引用看成绳子,对象看成气球。
Java 按值调用还是引用调用(传递)?
- 值传递:在调用函数时将实际参数复制一份(副本)传递到函数中,这样函数对参数进行修改,不会影响实际参数(实际是对副本进行操作)。
- 引用传递:在调用函数时将实际参数的地址传递到函数中,这样函数对参数进行修改,会影响实际参数。
值传递 | 引用传递 | |
根本区别 | 会创建副本(Copy) | 不创建副本(直接使用参数) |
导致结果 | 形参和实参本质互不影响 | 形参影响实参 |
ps:实参与形参:实际参数是调用有参方法的时候真正传递的内容(传给方法的值),而形式参数是用于接收实参内容的参数。
Java 总是按值传递,分析如下:
- 基本类型:像 int ,double等基本数据类型在参数传递时并没有传进变量本身,而是创建了一个新的相同数值的变量, 函数修改这个新变量并没有影响原来变量的数值。
基本类型 - 值传递
- 引用类型(对象):为什么对象a的值改变了呢?因为虽然也是按值传递,复制了一份新的引用但是指向的对象是同一个,修改后会影响原对象!这种方式假如在函数内修改 a=null; 只是把复制的引用与对象的联系断开,不影响函数外与实际对象。
引用类型 - 值传递
另外比较常见的例子:
public static void main(String[] args) { int[] arr = { 1, 2, 3, 4, 5 }; System.out.println(arr[0]); // 1 change(arr); System.out.println(arr[0]); // 0 } public static void change(int[] array) { // 将数组的第一个元素变为0 array[0] = 0; }
上边的代码中array作为arr的引用的拷贝,但两者都是指向堆中的数组对象,所以,外部对引用对象的改变会反映到对象本身。
对比看一下,如果是引用传递呢?因为引用传递是不复制的,直接使用参数,如下图:这时候函数把指针a=null就指针就置空了,函数外也无法再通过指针访问对象了
引用传递
综上所述:Java是值传递,即使传的是引用也不是引用传递,值的内容为对象的引用(赋值修改,但是对象本身没有改变)。
JDK8新特性?
- lambda 表达式:允许把函数作为参数传递到方法,简化匿名内部类代码。
- 函数式接口:使用 @FunctionalInterface 标识,有且仅有一个抽象方法,可被隐式转换为 lambda 表达式。
- 方法引用:可以引用已有类或对象的方法和构造方法,进一步简化 lambda 表达式。
- 接口:接口可以定义 default 修饰的默认方法,降低了接口升级的复杂性,还可以定义静态方法。
- 注解:引入重复注解机制,相同注解在同地方可以声明多次。注解作用范围也进行了扩展,可作用于局部变量、泛型、方法异常等。
- 类型推测:加强了类型推测机制,使代码更加简洁。
- Optional 类:处理空指针异常,提高代码可读性。
- Stream 类:引入函数式编程风格,提供了很多功能,使代码更加简洁。方法包括 forEach 遍历、count 统计个数、filter 按条件过滤、limit 取前 n 个元素、skip 跳过前 n 个元素、map 映射加工、concat 合并 stream 流等。
- 日期:增强了日期和时间 API,新的 java.time 包主要包含了处理日期、时间、日期/时间、时区、时刻和时钟等操作。
- JavaScript:提供了一个新的 JavaScript 引擎,允许在 JVM上运行特定 JavaScript 应用。
注意lamda表达式的优缺点和应用场景:
- 优点:1. 简洁。2. 非常容易并行计算。3. 可能代表未来的编程趋势。
- 缺点:1. 若不用并行计算,很多时候计算速度没有比传统的 for 循环快。(并行计算有时需要预热才显示出效率优势)2.** 不容易调试**。3. 若其他程序员没有学过 lambda 表达式,代码不容易让其他语言的程序员看懂。例子:List<int> evenNumbers = list.FindAll(i => (i% 2) == 0);
应用场景:Lamda表达式主要用于替换以前广泛使用的匿名内部类,各种回调,比如事件响应器、传入Thread类的Runnable等。
(1)使用() -> {} 替代匿名类
//Before Java 8: new Thread(new Runnable() { @Override public void run() { System.out.println("Before Java8 "); } }).start(); //Java 8 way: new Thread(() -> System.out.println("In Java8!")); // Before Java 8: JButton show = new JButton("Show"); show.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { System.out.println("without lambda expression is boring"); } }); // Java 8 way: show.addActionListener((e) -> { System.out.println("Action !! Lambda expressions Rocks"); });
(2)使用内循环替代外循环
外循环:描述怎么干,代码里嵌套2个以上的for循环的都比较难读懂;只能顺序处理List中的元素;
内循环:描述要干什么,而不是怎么干;不一定需要顺序处理List中的元素
//Before Java 8: new Thread(new Runnable() { @Override public void run() { System.out.println("Before Java8 "); } }).start(); //Java 8 way: new Thread(() -> System.out.println("In Java8!")); // Before Java 8: JButton show = new JButton("Show"); show.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { System.out.println("without lambda expression is boring"); } }); // Java 8 way: show.addActionListener((e) -> { System.out.println("Action !! Lambda expressions Rocks"); });
(3)支持函数编程
为了支持函数编程,Java 8加入了一个新的包java.util.function,其中有一个接口java.util.function.Predicate是支持Lambda函数编程
(4)用管道方式处理数据更加简洁
Java 8里面新增的Stream API ,让集合中的数据处理起来更加方便,性能更高,可读性更好
jar包和war包的主要区别
- 概念:
(1)jar包:JAR包是类的归档文件,JAR 文件格式以流行的 ZIP 文件格式为基础。与 ZIP 文件不同的是,JAR 文件不仅用于压缩和发布,而且还用于部署和封装库、组件和插件程序,并可被像编译器和 JVM 这样的工具直接使用。
(2)war包:war包是JavaWeb程序打的包,war包里面包括写的代码编译成的class文件,依赖的包,配置文件,所有的网站页面,包括html,jsp等等。一个war包可以理解为是一个web项目,里面是项目的所有东西。
- 目录结构:
(1)jar包里的com里放的就是class文件,配置文件,但是没有静态资源的文件,大多数 JAR 文件包含一个 META-INF 目录,它用于存储包和扩展的配置数据,如安全性和版本信息。
(2)war包里的WEB-INF里放的class文件和配置文件,META-INF和jar包作用一样,但是war包里还包含静态资源的文件。
- 项目部署:
(1)部署普通的spring项目用war包就可以
(2)部署springboot项目用jar包就可以,因为springboot内置tomcat。
- 总结不同:
(1)war包和项目的文件结构保持一致,jar包则不一样。
(2)jar包里没有静态资源的文件(index.jsp)