Java面向对象之类的成员
今天继续Java系列的面向对象,之前写过对象的内存分析,今天看下类的成员。
# 属性
# 成员变量的声明
语法格式如下:
[修饰符1] class 类名{
[修饰符2] 数据类型 成员变量名 [= 初始化值];
}
2
3
格式说明:
- 位置:必须在类中方法外
- 修饰符:常用的权限修饰符有private、缺省、protected、public,其他修饰符有static、final
- 数据类型:任何基本数据类型(如int、Boolean)或任何引用数据类型都可
- 成员变量名:属于标识符,符合命名规则和规范即可
- 初始化值:根据情况可以显式赋值,也可以不赋值使用默认值
举例:
public class Person{
private int age; //声明private变量age
public String name = “Lila”; //声明public变量name
}
2
3
4
# 修饰符
权限修饰符有:public、protected、缺省、private。
作用范围如下:
修饰符 | 本类 | 本包 | 其他包的子类 | 其他包的非子类 |
---|---|---|---|---|
private | √ | × | × | × |
缺省 | √ | √(本包的子类非子类都可见) | × | × |
protected | √ | √(本包的子类非子类都可见) | √(其他包仅限于子类中可见) | × |
public | √ | √ | √ | √ |
外部类有:public和缺省。
成员变量、成员方法等有:public、protected、缺省、private。
外部类要跨包使用必须是public,否则仅限于本包使用:
- 外部类的权限修饰符如果缺省,本包使用没问题
- 外部类的权限修饰符如果缺省,跨包使用有问题 成员的权限修饰符问题:
- 本包下使用:成员的权限修饰符可以是public、protected、缺省
- 跨包下使用:要求严格
- 跨包使用时,如果类的权限修饰符缺省,成员权限修饰符>类的权限修饰符也没有意义
# 成员变量和局部变量
成员变量在方法体外类体内声明,局部变量在方法体内等位置声明。
变量分类:
其中static可以将成员变量分为两大类,静态变量和非静态变量。其中静态变量又称为类变量,非静态变量又称为实例变量或者属性。
实例变量和局部变量的对比:
- 相同点。
- 变量声明的格式相同: 数据类型 变量名 = 初始化值
- 变量必须先声明后初始化再使用
- 变量都有其对应的作用域,只在其作用域内是有效的
- 不同点。
- 声明位置和方式:实例变量在类中方法外,局部变量在方法体中或方法的形参列表、代码块中
- 存储位置:实例变量在堆中,局部变量在栈中
- 生命周期:实例变量和对象的生命周期一样,随对象的创建而存在,随对象被GC回收而消亡且每一个对象的实例变量都是独立的;局部变量和方法调用的生命周期一样,每一次方法被调用而存在,随方法执行的结束而消亡且每一次方法调用也是独立的
- 作用域:实例变量通过对象就可以使用,本类中直接调用,其他类中用对象.实例变量,局部变量出了作用域就不能使用
- 修饰符:实例变量有public,protected,private,final,volatile,transient等,局部变量为final
- 默认值:实例变量有默认值,局部变量没有默认值,必须手动初始化,其中形参比较特殊,靠实参初始化
对象属性的默认值初始化赋值:当一个对象被创建时,会对其中各种类似的成员变量自动进行初始化赋值,具体如下:
# 对象数组
数组的元素可以是基本数据类型,也可以是引用数据类型,当元素是引用类型中的类时就被称为对象数组。
注:对象数组首先要创建数组对象本身,即确定数组的长度,然后再创建每一个元素对象,如果不创建那么数组的元素的默认值就是null,很容易出现空指针异常NullPointerException。
# 方法
方法是类或对象行为特征的抽象,用来完成某个功能操作,将功能封装为方法的目的是可以实现代码重用来减少冗余简化代码。Java中的方法不能独立存在,所有的方法都必须定义在类里。
# 声明
声明方法的语法格式:
[修饰符] 返回值类型 方法名([形参列表])[throws 异常列表]{
方法体的功能代码
}
2
3
- 修饰符:可选的,方法的修饰符有很多,例如public、protected、private、static、abstract、native、final、synchronized等,其中根据是否有static可以将方法分为静态方法和非静态方法,其中静态方法也称为类方法,非静态方法也称为实例方法
- 返回值类型:表示方法运行的结果的数据类型,方法执行后将结果返回到调用者,无返回值时则用void,有返回值则声明返回值的类型(可以是任意类型),与方法体中的return搭配使用
- 方法名:属于标识符,命名时遵循标识符命名规则和规范即可
- 形参列表:表示完成方法体功能时需要外部提供的数据列表,可以包含零个或多个参数,无论有没有参数()都不能省略
- throws异常列表:可选的
# 调用
方法通过方法名被调用,且只有被调用时才会执行。
方法调用语法格式为:对象.方法名([实参列表])
。
注:必须先声明后使用,且方法必须定义在类的内部,调用一次就执行一次不调用就不执行,方法中可以调用类中的方法或属性,不可以在方法内部定义方法。
# 内存分析
方法在没有被调用的时候,都在方法区中的字节码文件中存储。方法被调用的时候,需要进入到栈内存中运行,方法每调用一次就会在栈中有一个入栈动作,即给当前方法开辟一块独立的内存区域用于存储当前方法的局部变量的值。当方法执行结束后会释放该内存,称为出栈,如果方法有返回值,就会把结果返回到调用处,如果没有返回值就直接结束回到调用处继续执行下一条指令(栈结构为先进后出后进先出)。
举例:
public class Person {
public static void main(String[] args) {
Person p1 = new Person();
p1.eat();
}
public static void eat() {
sleep();
System.out.println("人吃饭");
}
public static void sleep(){
System.out.println("人睡觉");
doSport();
}
public static void doSport(){
System.out.println("人运动");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
内存分析如下:
# 方法重载
方法重载是指在同一个类中,允许存在一个以上的同名方法,只要参数列表不同即可,参数列表不同意味着参数个数或参数类型的不同。
重载的特点:与修饰符和返回值类型无关,只看参数列表,且参数列表必须不同,调用时根据方法参数列表的不同来区别。
重载方法调用:jvm通过方法的参数列表调用匹配的方法,先找个数、类型最匹配的,再找个数和类型可以兼容的,如果同时多个方法可以兼容将会报错。
举例:
//System.out.println()方法就是典型的重载方法,其内部的声明形式如下:
public class PrintStream {
public void println(byte x)
public void println(short x)
public void println(int x)
public void println(long x)
public void println(float x)
public void println(double x)
public void println(char x)
public void println(double x)
public void println()
}
public class HelloWorld{
public static void main(String[] args) {
System.out.println(3);
System.out.println(1.2f);
System.out.println("hello!");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 方法重写
父类的所有方法在子类都会被继承,但是当某个方法被继承到子类之后,子类觉得父类原来的实现不适合自己当前的类,这时候子类可以对从父类中继承来的方法进行改造,就称之为方法的重写 (override、overwrite),也称为方法的重置、覆盖。重写后,在程序执行时子类的方法将覆盖父类的方法。
重写的要求:
- 子类重写的方法必须和父类被重写的方法具有相同的方法名称、参数列表。
- 子类重写的方法返回值类型不能大于父类被重写的方法的返回值类型(例如Student<Person,注:如果返回值类型是基本数据类型和void,那么必须相同)。
- 子类重写的方法使用的访问权限不能小于父类被重写的方法的访问权限(public>protected>缺省>private,注:父类私有方法不能重写,挎包的父类缺省的方法也不能重写)。
- 子类方法抛出的异常不能大于父类被重写方法的异常。
此外,子类与父类中同名同参数的方法必须同时声明为非static的(即为重写),或者同时声明为static的(不是重写),因为static方法属于类的,子类无法覆盖父类的方法。
举例:
//父类
public class Phone {
public void sendMessage(){
System.out.println("发短信");
}
public void call(){
System.out.println("打电话");
}
public void showNum(){
System.out.println("来电显示号码");
}
}
//子类
public class SmartPhone extends Phone{
//重写父类的来电显示功能
@Override
public void showNum(){
System.out.println("显示来电姓名");
System.out.println("显示头像");
}
//重写父类的通话功能
@Override
public void call() {
System.out.println("语音通话 或 视频通话");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
注:@Override用来写在方法上面,来检测是不是满足重写方法的要求,这个注解就算不写但只要满足要求,也是正确的重写。但建议保留,这样编译器可以帮助检查格式,另外也可以让增加阅读性,更清晰的知道这是一个重写的方法。
总结:方法的重载是方法名相同,形参列表不同,不看返回值的类型,是在同一个类中。方法的重写是重写父类被继承的方法,是在父子类中。
# 可变形参
在jdk5.0中提供了可变形参机制,即当定义一个方法时,形参的类型可以确定,但是形参的个数不确定,那么可以考虑使用可变个数的形参。
格式为:方法名(参数的类型名 ...参数名)
。
特点:
- 可变参数:方法参数部分指定类型的参数个数是可变多个
- 可变个数形参的方法与同名的方法之间彼此构成重载
- 可变参数方法的使用与方法参数部分使用数组是一致的,二者不能同时声明,否则报错
- 方法的参数部分有可变形参,需要放在形参声明的最后
- 在一个方法的形参中,最多只能声明一个可变个数的形参
举例:n个字符串进行拼接,每一个字符串之间使用某字符进行分割,如果没有传入字符串,那么返回空字符串""。
public class StringTools {
String concat(char seperator, String... args){
String str = "";
for (int i = 0; i < args.length; i++) {
if(i==0){
str += args[i];
}else{
str += seperator + args[i];
}
}
return str;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# 方法的参数传递机制
在定义方法时,方法名后面括号()中声明的变量称为形式参数,简称形参。在调用方法时,方法名后面括号()中的使用的值/变量/表达式称为实际参数,简称实参。
Java里方法的参数传递方式只有一种,那就是值传递,即将实际参数值的副本(复制品)传入方法内,而参数本身不受影响。
- 形参是基本数据类型:将实参基本数据类型变量的“数据值”传递给形参
- 形参是引用数据类型:将实参引用数据类型变量的“地址值”传递给形参
# 递归方法
方法自己调用自己的现象就称为递归,递归可分为直接递归和间接递归。
直接递归:方法自己调用自己。
public void methodA(){
methodA();
}
2
3
间接递归:可以理解为A()方法调用B()方法,B()方法调用C()方法,C()方法调用A()方法。
public static void A(){
B();
}
public static void B(){
C();
}
public static void C(){
A();
}
2
3
4
5
6
7
8
9
说明:
- 递归方法包含了一种隐式的循环
- 递归方法会重复执行某段代码,但这种重复执行无需循环控制
- 递归一定要向已知方向递归,否则这种递归就变成了无穷递归停不下来,类似于死循环,最终会发生栈内存溢出
举例:递归方法计算n!。
public int multiply(int num){
if(num == 1){
return 1;
}else{
return num * multiply(num - 1);
}
}
2
3
4
5
6
7
最后,递归调用会占用大量的系统堆栈,内存占用多,在递归调用层次多时速度要比循环慢得多,所以在使用递归时要谨慎。在要求高性能的情况下应尽量避免使用递归,递归调用即花时间又耗内存,可以考虑使用循环迭代。
# 构造器
在new完对象时,所有成员变量都是默认值,如果需要赋别的值,就需要挨个为它们再赋值,这样太麻烦了。那么能不能在new对象时直接为当前对象的某个或所有成员变量直接赋值呢,答案是肯定的,Java给我们提供了构造器(Constructor)
,也称为构造方法
。
# 作用
new对象并在new对象的时候为实例变量赋值。
# 语法格式
构造器格式如下:
[修饰符] class 类名{
[修饰符] 构造器名(){
// 实例初始化代码
}
[修饰符] 构造器名(参数列表){
// 实例初始化代码
}
}
2
3
4
5
6
7
8
说明:
- 构造器名必须与它所在的类名相同
- 构造器没有返回值,所以不需要返回值类型,也不需要void
- 构造器的修饰符只能是权限修饰符,不能被其他任何修饰,比如不能被static、final、synchronized、abstract、native修饰,不能有return语句返回值
举例:
//定义类
public class Student {
private String name;
private int age;
//无参构造
public Student() {}
//有参构造
public Student(String n,int a) {
name = n;
age = a;
}
public String getName() {
return name;
}
public void setName(String n) {
name = n;
}
public int getAge() {
return age;
}
public void setAge(int a) {
age = a;
}
public String getInfo(){
return "姓名:" + name +",年龄:" + age;
}
}
//测试类
public class TestStudent {
public static void main(String[] args) {
//调用无参构造创建学生对象
Student s1 = new Student();
//调用有参构造创建学生对象
Student s2 = new Student("张三",23);
System.out.println(s1.getInfo());
System.out.println(s2.getInfo());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
注:当没有显式的声明类中的构造器时,系统会默认提供一个无参的构造器并且该构造器的修饰符默认与类的修饰符相同,当显式的定义类的构造器以后,系统就不再提供默认的无参的构造器了,另外在类中至少会存在一个构造器,构造器是可以重载的。
# 代码块
如果成员变量想要初始化的值不是一个硬编码的常量值,而是需要通过复杂的计算或读取文件或读取运行环境信息等方式才能获取的一些值,此时可以考虑代码块(或初始化块)。
代码块的作用是对Java类或对象进行初始化,代码块若有修饰符,则只能被static修饰,这时称为静态代码块,没有使用static修饰的为非静态代码块。
# 静态代码块
如果想要为静态变量初始化,可以直接在静态变量的声明后面直接赋值,也可以使用静态代码块。
静态代码块的格式如下:
[修饰符] class 类{
static{
静态代码块
}
}
2
3
4
5
静态代码块的特点:
- 可以有输出语句
- 可以对类的属性、类的声明进行初始化操作
- 不可以对非静态的属性初始化,即不可以调用非静态的属性和方法
- 若有多个静态的代码块,那么按照从上到下的顺序依次执行
- 静态代码块的执行要先于非静态代码块
- 静态代码块随着类的加载而加载,且只执行一次
# 非静态代码块
非静态代码块的作用和构造器一样,也是用于实例变量的初始化等操作。如果多个重载的构造器有公共代码,并且这些代码都是先于构造器其他代码执行的,那么可以将这部分代码抽取到非静态代码块中减少冗余代码。
非静态代码块的格式如下:
[修饰符 class 类{
{
非静态代码块
}
[修饰符] 构造器名(){
// 实例初始化代码
}
[修饰符] 构造器名(参数列表){
// 实例初始化代码
}
}
2
3
4
5
6
7
8
9
10
11
非静态代码块的特点:
- 可以有输出语句
- 可以对类的属性、类的声明进行初始化操作
- 除了调用非静态的结构外,还可以调用静态的变量或方法
- 若有多个非静态的代码块,那么按照从上到下的顺序依次执行
- 每次创建对象的时候,都会执行一次。且先于构造器执行
举例:
public class Test {
private static String country;
private String name;
{
System.out.println("非静态代码块");
}
static {
System.out.println("静态代码块");
}
public Chinese(String name) {
this.name = name;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
总结下实例变量的赋值顺序: 声明成员变量的默认初始化-->显式初始化、多个初始化块依次被执行(同级别下按先后顺序执行)-->构造器再对成员进行初始化操作-->通过对象.属性或对象.方法的方式,可多次给属性赋值。
今天的内容就到这里,喜欢的话点个关注吧,下篇见!
🎁 公众号
小伙伴们大家好,上方扫码关注公众号「大数据技术开发」,与你分享我的成长历程与技术感悟~