1 方法重载(overload)
- 方法重载的作用?
同时满足用户的不同需求。 同一个方法,用户可以传入不同的值进行操作(比如学信网按照不同的证件查信息即一个查找方法,传不同类型的参数)。
-
方法重载的条件?
- 必须发生在同一个类体里面
- 方法名需要完全一致(大小写不同也不行,Java区分大小写)
- 方法的参数不同(包括参数列表类型/个数/顺序不同。因为参数名不被保存在.class文件里,所以如果参数列表中只是参数名不同,其他都相同时,无法构成重载,报错“方法重复定义”)
-
综上,方法重载对修饰符(访问权限、静态) 和 返回类型没有要求,只与参数类型个数顺序相关。
-
如果一个方法参数列表可以传入int,传入short、byte、char、int都行,不能传入比int更大的数据类型。如果传入的参数没有匹配数据类型的方法,从就近(向更大的范围)的方法参数列表匹配。
-
例题
//1 下面哪些方法满足方法重载?
//1245
//1
public static void test(){
System.out.println(1);
}
//2
public int test(int x){
return 2;
}
//3
public static void test1(int x){
System.out.println(3);
}
//4
public static String test(String x){
return "4";
}
//5
public static void test(double x,int y){
System.out.println(5);
}
//2 问下面的代码______和_____满足方法重载?
// B类从A类继承得到的test方法 和 B类自己的test方法 满足方法重载
class A{
public void test(){
System.out.println("A类的test方法");
}
}
class B extends A{
//继承而来的test()
public void test(int x){
System.out.println("B类的test方法");
}
}
2 方法重写(覆盖)(override)
-
方法重写(覆盖)的作用
可将方法重写(覆盖)类比为《进化论》。Sun公司认为子类重新实现的方法应该更加优秀,且更加优秀的方法应该给更多的人使用,利于Java的良性发展。 -
综上,可以总结对重写时方法的一些要求:
public | void test() | throws Exception |
---|---|---|
脑袋 | 躯干 | 尾巴 |
脑袋:子类重写时的访问权限修饰符一定要比父类的访问权限修饰符更开放。修饰为静态的方法不允许被重写。
躯干:JDK5.0之前时“返回值+方法名”必须与父类统一。JDK5.0开始,允许协变返回类型,即允许子类使用自己的类作为返回类型(例题1)。无论什么版本,参数列表一定要相同(子父类参数列表中参数的类型有继承关系也不行)。
尾巴:异常处理部分,重写的异常抛出的范围需要比父类抛的更小(如果父类是Exception这样比较大的异常类,子类抛出几个小一些异常也可以),最好不抛。
-
方法重写(覆盖)有哪些条件
- 必须发生在有继承关系的两个类中的子类(子类在继承得到父类的某个方法后,觉得父类的实现不好,于是在子类里重新实现一遍)
- 方法名需要完全一致(大小写不同也不行,Java区分大小写)
- JDK5.0之前的版本返回类型必须与父类一致,5.0以后支持协变返回类型
- 参数列表一定相同(个数、顺序、参数类型)
- 子类的访问权限修饰符必须大于父类
- 父类若有非运行时异常抛出,子类的非运行时异常必须更少或者没有
-
如果想在重写的方法中使用父类的公共方法和变量,可以使用 “super.” 来调用这些方法和变量。
-
父类的私有属性、构造方法子类都拿不到。
-
另外从JDK5.0开始,方法重写时,可以在子类要重写的方法上面加@Override(注解),表示下面的方法一定要重写父类的某个方法。(注释是给人看的,注解是给机器看的)
-
子类中,只有方法存在重写覆盖,变量不存在覆盖,有几个就存几个,重名也可以。区分他们的方式是如何调用。(例题2)
-
例题
//1.协变返回类型的使用
class Animal{
public Animal givingBirth(){
Animal aa = new Animal();
return aa;
}
}
class Dog extends Animal{
@Override
public Dog givingBirth(){
Dog dd = new Dog();
return dd;
}
}
class Cat extends Animal{
@Override
//jdk5.0开始,返回类型可以变成父类方法返回类型的子类类型
public Cat givingBirth(){
Cat cc = new Cat();
return cc;
}
}
//2.继承的同名变量问题
class A{
String x;
}
class B extends A{
String x;
public Teacher(String x){
System.out.println(super.x);//输出父类的x
System.out.println(this.x);//输出子类的x
System.out.println(x);//输出方法参数的x
}
}
使用协变返回类型时需要注意,返回类型需要使用子类的类型而非父类。
- 面试题:override和overload之间的区别(重点,注意要点对点答)
名称 | 含义 | 发生的位置 | 对返回类型的要求 | 对参数要求 |
---|---|---|---|---|
overload | 方法重载 | 同一个类 | 没有要求 | 参数不同(类型/个数/顺序) |
override | 方法覆盖(重写) | 有继承关系的两个类 | JDK5.0之前必须相同,JDK5.0开始允许协变返回类型 | 参数相同 |
3 构造方法
构造方法是创建对象时调用的方法。所有类中都有构造方法。创建对象是一个复杂的过程,在创建的最后,底层才调用了构造方法,在这之前底层还调用了其他的方法。注意Java中没有构造函数的概念,函数这个名词应用于C++中。构造方法也可以重载。
-
构造方法的作用
构造方法语法的出现是为了方便程序员的开发,可以实现在创建对象的同时直接给属性进行赋值。 -
既然有了构造方法,还需要属性的setter方法吗?
需要,构造方法只是初始化属性,后期如果想修改这个引用中的属性,还是需要使用set方法。 -
构造方法的特点
- 没有任何返回类型
- 构造方法的名字必须与类名相同
-
注意,Java中只要是个类就一定有构造方法,即使没有写,系统也会提供一个默认的无参、空体的构造方法。
-
如果想在创建对象的同时直接给属性赋值,需要自己写构造方法。一旦写出自己的构造方法,默认的构造方法将不再提供。一般在声明了有参有体的构造方法后,作为习惯也要再重新声明一个无参空体的默认构造方法。
-
关于构造方法的第一行(用于共享代码,提高代码重用性) ★
默认第一行调用super()方法,也可在第一行显式调用具有参数的super()或有无参均可的this()。这些被调用的语句必须在第一行调用,也就是说他们不可以共存。如果第一行调用了this(),即不再向上递到父类执行构造方法。 一定要注意如果子类方法中第一行没有super()也没有this(),一定要先添加出来看一下。-
super()
表示执行本构造方法之前,默认先去执行父类无参的构造方法。即在执行子类时从辈分最高的父类依次向下执行无参的构造方法(super()在构造方法中的递归递向上,再归着执行下来)。即使是程序员写出的辈分最高的父类也默认调用这个方法,它的父类是Object基类。它的作用是直接引用父类构造方法中的内容,相当于把父类这个构造方法中代码复制到子类里来,其实执行还是在子类中执行。
super()与super.的区别是:super()使用于构造方法首行,用于要执行本构造方法之前将父类某个构造方法(看参数列表传的值)中的内容“复制”到子类本方法中执行,完成代码共享减少代码量,并非直接在父类中执行。super.用于调用父类的成员,即使方法或者变量被重写,它依然调用的是父类的成员。
如果要在父类无参方法被自定义有参构造方法覆盖的情况下要定义子类,首先需要
1 自行去父类提供无参构造方法
2 或者向子类super()里传参数,指定它找父类的哪一个有参构造方法
以这样为前提,子类才可以顺利创建,否则报错。 -
this()
表示要执行本构造方法前,先执行本类其他的构造方法。具体执行本类的哪一个构造方法,看括号中的参数类型。在调用this()时要注意避免在构造方法中的递归调用,即在一个构造方法中调用另外一个构造方法,使方法之间调用形成递归。this()与this.的区别是:this()使用于构造方法首行,用于执行本构造方法之前,将本类某个构造方法(看参数列表传的值)中的内容“复制”到本法中执行,完成代码共享减少代码量;this.用于调用本类的成员。
-
构造方法可以重载,不能重写,因为重写的前提是先继承得到,但是父类的构造方法不能被子类继承,所以不能覆盖。
-
例题
-
//1 打印语句输出
public class Test1{
public static void main(String[] args){
Demo d1 = new Demo();
/**
打印结果为1,,调用无参构造方法给成员变量str赋值
*/
System.out.println(d1.str);
Demo d2 = new Demo("Hello world.");
/**
打印结果为null,调用有参构造方法就近给局部变量str赋值,
局部变量在方法结束后消亡
*/
System.out.println(d2.str);
Sample ss = new Sample("Hello world.");
/**
打印结果为1,调用有参子类构造方法,因为没有声明super(),
所以默认调用无参的父类构造方法,给成员变量str赋值,
另外子类继承了父类的str变量,得到输出
*/
System.out.println(ss.str);
Sample1 ss = new Sample1("Hello world.");
/**
打印结果为null,传入构造方法后super将字符串传入父类的有参构造,
但是有参构造为局部变量,方法结束即消亡
子类构造方法中的str也是局部变量,方法结束即消亡
*/
System.out.println(ss.str);
}
}
class Demo{
String str;//成员变量
public Demo(){
str = "1";//成员变量
}
public Demo(String str){
str = "2";//局部变量,想变为成员变量,str前面加this.
}
}
class Sample extends Demo{
//虽然没写,但是子类依然继承了str变量
public Sample(String str){
//默认执行父类的无参构造方法,赋值成员变量↓↓↓↓↓↓↓↓
//super(); ---> str = "1";
str = "OK";//局部变量,想变为成员变量,str前面加this.
}
}
class Sample1 extends Demo{
//虽然没写,但是子类依然继承了str变量
public Sample1(String str){
//显式调用了str
super(str);//传入str的变量,但是父类中的str为局部变量,方法结束即消亡
str = "OK";//局部变量,想变为成员变量,str前面加this.
}
}
所以在执行尤其是子父类均有的方法时,一定要注意如果没有super()一定要自动补齐,另外尤其注意重名变量的作用域、是否消亡的问题。
4 参数传值
Java中只有值传递,基本数据类型传值,引用数据类型传地址。
- 结合简书大佬@androidjp的博文,Java的内存分为栈内存和堆内存。方法区可以理解为:主要存放静态数据、全局 static 数据、String字面值的常量池和其他常量。这块内存在程序编译时就已经分配好,并且在程序整个运行期间都存在。总的来说:堆和栈针对非静态数据,而方法区针对静态数据。栈与堆都是Java用来在RAM中存放数据的地方。与C++不同,Java自动管理栈和堆,程序员不能直接地设置栈或堆。
栈:
简单理解:堆栈(stack)是操作系统在建立某个进程或者线程(在支持多线程的操作系统中是线程)为这个线程建立的存储区域,该区域具有先进后出的特性。
特点:存取速度比堆要快,仅次于直接位于CPU中的寄存器。栈中的数据可以共享(意思是:栈中的数据可以被多个变量共同引用)。
缺点:存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。
相关存放对象:①一些基本类型的变量(int, short, long, byte, float, double, boolean, char)和对象句柄(例如:在函数中定义的一些基本类型的变量和对象的引用变量)。②方法的形参 直接在栈空间分配,当方法调用完成后从栈空间回收。
特殊:①方法的引用参数,在栈空间分配一个地址空间,并指向堆空间的对象区,当方法调用完成后从栈空间回收。②局部变量new出来之后,在栈空间和堆空间中分配空间,当局部变量生命周期结束后,它的栈空间立刻被回收,它的堆空间等待GC回收。
栈区:
每个线程包含自己的一个栈区,栈中只保存基本数据类型的对象和自定义对象的引用。
每个栈中的数据(基本类型和对象引用)都是私有的,其他栈不可访问。
栈 = 基本类型变量区 + 执行环境上下文 + 操作指令区(存放操作指令)
堆:
简单理解:每个Java应用都唯一对应一个JVM实例,每一个JVM实例唯一对应一个堆。应用程序在运行中所创建的所有类实例或者数组都放在这个堆中,并由应用所有的线程共享。Java中分配堆内存是自动初始化的,Java中所有对象的存储空间都是在堆中分配的,但这些对象的引用则是在栈中分配,也就是一般在建立一个对象时,堆和栈都会分配内存。
特点:可以动态地分配内存大小、比较灵活,生存期也不必事先告诉编译器,Java的垃圾收集器会自动收走这些不再使用的数据。在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。
缺点:由于要在运行时动态分配内存,存取速度较慢。
主要存放:①由new创建的对象和数组 ;②this
特殊:引用数据类型(需要用new来创建),既在栈空间分配一个地址空间,又在堆空间分配对象的类变量。
堆区:
存储的全是对象,每个对象都包含一个与之对应的class信息(我们常说的类类型,Clazz.getClass()等方式获取),class目的是得到操作指令。
JVM只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身。(这里的‘对象’,就不包括基本数据类型)
方法区
又称为‘静态区’,和堆一样,被所有的线程共享。
方法区包含所有的class和static变量。
-
对于存在于方法中的局部变量(自动变量),当程序调用方法时,系统会为该方法建立一个方法栈,这些变量就放在方法栈中,当方法结束系统会释放方法栈,这些变量随着这个方法栈的销毁而自动消亡,这是局部变量只能在方法中有效的原因。
- 当变量为基本数据类型,其变量名和值存储在方法栈。
- 当变量为引用数据类型,所声明的引用放在方法栈,这个引用指向的对象存储在堆内存。
-
类体中的成员变量(基本、引用数据类型)的存储也遵循上面的存储方式。与局部变量不同的是,因在程序中,非静态的成员变量依赖于对象存在,当声明对象时,类中的成员变量加载进入内存并赋默认值。每个成员变量存在一个有“指向堆内存中对应对象地址的指针”类似作用的连接。当对象执行结束之后,类也跟着调用这个方法的生命周期消亡。
-
例题(结合每个程序下面的内存图理解,其中上半部分左边是栈内存表示引用,右边是堆内存表示对象)
//1
public class Test1{
public static void main(String[] args){
int a = 30;
change(a);
System.out.println(a);//--->30
}
public static void change(int x){
//int x = a; //将参数列表在方法体内部补全
x = 50;
}
}
- 第一题的内存图
//2
public class Test2{
public static void main(String[] args){
int a = 60;
int b = 90;
change(a,b);
System.out.println(a); //--->60
System.out.println(b); //--->90
}
public static void change(int x,int y){
//int x = a;
//int y = b;
//x y z是局部变量
int z = x;
x = y;
y = z;
}
}
- 第二题的内存图
//3
public class Test3{
public static void main(String[] args){
String a = new String("O");
String b = new String("K");
change(a,b);
System.out.println(a);//--->输出O
System.out.println(b);//--->输出K
}
public static void change(String x,String y){
//String x = a;
//String y = b;
//x y z 是局部变量
String z = x;
x = y;
y = z;
}
}
- 第三题的内存图
//4
public class Test4{
public static void main(String[] args){
Student s = new Student(30);
change(s);
System.out.println(s.age);//--->55
}
public static void change(Student stu){
//Student stu = s;
//stu是局部变量
stu.age = 55;
}
}
class Student{
int age;
public Student(int age){
this.age = age;
}
}
- 第四题的内存图(Step3在最后与方法栈一起消亡)
//5
public class Test5{
public static void main(String[] args){
Student s = new Student(30);
change(s);
System.out.println(s.age);//--->30
}
public static void change(Student stu){
//Student stu = s;
//stu是局部变量,但是在这里new了新的对象
stu = new Student(55);
}
}
class Student{
int age;
public Student(int age){
this.age = age;
}
}
- 第五题的内存图(Step3在Step4之前消亡)
//6
public class Test6{
public static void main(String[] args){
TestForm ff = new TestForm();
ff.setId(2);
int x = 2;
change(x,ff);
System.out.println("x=" + x);
System.out.println("ff.getId():" + ff.getId());
}
private static void change(int id,TestForm form){
//int id = x;
//TestForm form = ff;
//id form是局部变量
id = 4;
form.setId(4);
}
}
class TestForm{
private int id;
public void setId(int id){
this.id = id;
}
public int getId(){
return id;
}
}
- 第六题的内存图(Step6最后消亡)
//7
public class Test7{
public static void main(String[] args){
//如果方法没有声明static,就要创建对象调用它
Test7 t = new Test7();
A a = new A();
a.age = 10;
t.test(a);
System.out.println("a的年龄:" + a.age);//--->a的年龄:20
}
private void test(A x){
//x是局部变量
//A x = a;
x.age = 20;
System.out.println("x的年龄" + x.age);//--->x的年龄:20
}
}
class A{
int age;
}
- 第七题的内存图
public class Test8{
public static void main(String[] args){
A x = new A(0);
add(x);
System.out.println(x.value);
}
public static void add(A a){
//A a = x;
//a val是局部变量
int val = a.value;
val += 3;
a = new A(val);
}
}
class A{
int value;
public A(int value){
this.value = value;
}
}
- 第八题的内存图(Step3在Step6时消亡)
//9 附:注意,当循环时对指定下标进行操作时,一定使用for而不能使用foreach
//将数组中所有值赋为5
int[] data = new int[5];//0 0 0 0 0
//无法操作控制下标
for(int x : data){
x = 5;
}
//正确做法
for(int x = 0;x < data.length;x++){
data[x] = 5;
}