目录
2、cat.age = 3; cat.name = "小白";
(1)我们新建一个类时,这个类的内部是自带一个隐藏的无参构造器的。
(2)当我们自己定义一个有参构造器时,类自带的无参构造器会自动清除,此时如果想再使用无参构造器,就要显式的自己重新定义一下。
一、类和对象
1、为什么要有类和对象?
为了便于Java程序的管理,Java采用面向对象的编程思想(OOP)。
2、举例
现在有一个小猫(名字叫小白),我想用Java程序描述这个猫,就要用到类和对象。
class Cat{
int age;
String name;
}
如代码所示,这里我创建了一个Cat类,也就是创建了一个数据类型,就像int double String。Cat类里面有两个属性,age年龄和name名字。
这里的Cat类,就有了所有的猫共有的属性(年龄 ,名字)。
我们还可以定义猫的一些行为,这就是类中的方法。
如:
class Cat{
int age;
String name;
public void mew(){
System.out.println("喵喵叫~");
}
}
这里我定义了一个mew()方法,表示小猫叫的行为。
3、如何创建一个对象?
以上我定义的这些,都只是猫这个类,并不是某一个具体的对象。
我们需要将类实例化,才能创造出一个对象。
如:
public class Example01 {
public static void main(String[] args) {
Cat cat = new Cat();
}
}
这里我在主方法中创建了一个猫的对象。(new了一个对象)
Cat cat = new Cat();
这就相当于一个小猫的诞生。
我们还可以给它名字以及年龄(属性赋值),还可以控制小猫的行为(使用方法)。
public class Example01 {
public static void main(String[] args) {
Cat cat = new Cat();
cat.age = 3;
cat.name = "小白";
cat.mew();
}
}
这里我让它的年龄(age)为3,名字(name)为“小白”,然后使用它的mew()方法,来让它喵喵叫,输出:
4、如何访问对象属性以及方法?
使用“.”这个符号,就能访问一个对象的属性,或者使用其方法(这里还有访问修饰符的问题,后面说)。
比如,我想要知道这个小猫对象的名字,或者年龄,可以这样:
public class Example01 {
public static void main(String[] args) {
Cat cat = new Cat();
cat.age = 3;
cat.name = "小白";
cat.mew();
System.out.println(cat.name + cat.age);
}
}
使用System.out.println()然后用".'这个符号将cat.name和cat.age输入进去就可以了。
输出: 小白3
5、总结
可以见得,使用类和对象来管理Java中的内容,是非常方便的,只要定义一个类,并且创建对象,给每个不同的对象赋值就能管理很多内容。
这里需要注意,一个类不是一个对象。
就像我们都是人类,但是我们每一个人都是一个独立的对象(小明、小红)。
二、类和对象在内存中的存在形式(重要)
1、运行Cat cat = new Cat();
我们通过这条语句来认识类和对象在内存中的存在形式
这是我们java虚拟机的内存,大致分为三块,栈、堆、方法区(至于什么是栈、堆......以后再说,现在先知道就是两块不同区域)。
当我们创建一个对象时,内存里发生了什么事呢?
当我们创建一个对象,也就是
执行
Cat cat = new Cat();
这条语句时
内存中的堆里,会开辟这样一个空间:
这个空间有一个地址,我们假定是0x1111,这个空间,其实就是我们创建出来的对象本体,
这个空间里还有我们在Cat类中定义的属性,name、age,但是现在由于我们没有给定一个值,所以name和age在内存中都是默认值。
name 是 String类型,所以默认值为null;而age是int类型,所以默认是0;
此时,这条语句:
Cat cat = new Cat();
还没有执行完毕。
以上其实只是new Cat();这一步的操作。
而“Cat cat = ”还没有完成。
内存创建一个真正的对象空间以后,会在栈中创建一个cat 并设置一个地址,指向我们真正的对象。这也就是我们"Cat cat =" 的操作。" = "就是指向这个对象。
正是因为cat在内存中指向了0x1111这个真正的对象空间,我们才可以在代码中使用cat来当做我们新创建的这个对象。
cat作为对象
至此,"Cat cat = new Cat();"就运行完毕。
2、cat.age = 3; cat.name = "小白";
运行这条语句内存会发生什么变化呢?
对于age属性,我们定义的是int类型,在对象空间中,cat.age = 3;
我们会直接将3放在对象空间中的age区中,如图:
而执行cat.name = "小白"这条时,
由于name属性我们定义的是String类型,所以与int属性的处理有所区别。
Java程序会先在方法区中的常量池中(常量池是方法区中的一块小区域)查找是否有“小白”这个字符串,
显然是没有的,所以程序会在内存中创建一个“小白”字符串,这个“小白”有一个地址,我们假定是0x22
创建完成后,就会将name指向这个"小白"(即让name存储0x22这个地址)
这样,我们这个cat对象的name就是“小白”了。
3、什么是一个新的对象?
Cat cat1 = null;
cat1 = cat;
刚才我们创建了一个对象cat,现在我声明一个新的Cat—— cat1,先让其初始化为null,
然后再让其指向cat。
问题:cat1是一个新的对象吗?
答:不是。
它在内存中是这样的。
之前我提到 堆中的这个空间才是真正的对象 从图中可以看到,cat和cat1都只是指向了这同一个对象,所以,cat和cat1是同一个对象,cat1不是一个新对象。
4、为什么要了解这个内存机制?
不了解内存机制的话,会造成很多程序设计上的混乱。
看一下这个题:
如果不了解对象在内存中的机制,可能就会产生这样的疑惑:
我明明修改的是a.name为小明,为什么b.name也变成了小明?
我明明修改的是b.age为200,为什么a.age也变成了200?
我们学习了内存中的运行机制后,答案现在就显而易见了,因为a和b指向的是同一个对象,
所以不管是a修改还是b修改属性,都是内存中那个同一个对象在修改。
三、方法的调用机制(重要)
同样的,我们还要了解一个对象的方法(成员方法)的机制。
public class Example01 {
public static void main(String[] args) {
Cat cat = new Cat();
cat.age = 3;
cat.name = "小白";
//下面调用了cat的成员方法
cat.mew();
}
}
如图,我们在主方法中调用了cat对象的方法mew(),在内存中是这样调用的:
在栈中存在着主方法,以及其中的各行代码
当执行到cat.mew()这行时,栈中会出现一个新的空间:
当mew方法执行完毕后,这个方法就会被栈弹出(更具体的内容见数据结构),在方法区中消失。
然后回到main方法中继续执行没执行完的内容。
需要注意这里的mew方法没有返回值,如果成员方法有返回值,也是一样的,只不过多一个返回值到main方法中继续执行。
四、方法的使用细节
1、使用有参数的方法时,要传入对应的或者相兼容的参数
这里创建一个A1类,内有一个方法sum用于返回两个参数的和,如图
public class Example02 {
public static void main(String[] args) {
A1 a1 = new A1();
System.out.println(a1.sum(1, 2));
}
}
class A1{
public int sum(int a,int b){
return a + b;
}
}
输出:
这里传入的1,2都是int类型,如果我传入double类型会怎样?
报错了,编译器不允许通过。
就是说:如果需要int型参数,就不能传入double等更高精度的变量
而反过来是可以的:
所以,方法传入的参数可以是较低精度的参数,而不能是较高精度的参数。
2、方法想要返回多个值,则可以返回数组。
3、返回类型可以是任意类型,包括数组,和对象
4、方法内不能定义方法
5、同一个类中的方法可以直接调用
如图,这里定义了一个getAge方法,我们可以将mew方法改为图中内容
class Cat{
int age;
String name;
public void mew(){
System.out.println("喵喵叫~ " + "我今年" + getAge() + "岁");
}
public int getAge(){
return age;
}
}
输出:
可见,本类的方法可以在本类中直接使用。
6、不同类的方法需要先创建对象再使用
如图 我们的主类Example01和Cat类是不同类,所以要使用Cat类的mew方法则需要先创建一个对象,再用对象名 + “.”调用方法。(还有一个特殊情况,static静态方法,用类名+“.”调用 这篇不提)
而跨类调用方法还与访问修饰符有关,后面说。
public class Example01 {
public static void main(String[] args) {
Cat cat = new Cat();
cat.age = 3;
cat.name = "小白";
cat.mew();
}
}
五、方法的传参机制(重要)
1、基本数据的传递
public class Example03 {
public static void main(String[] args) {
int a = 1;
int b = 2;
swap(a,b);
}
public static void swap(int a, int b) {
//这个方法的作用是交换a b的值
int t;
t = a;
a = b;
b = t;
}
}
这里我写了一个交换a b的值的方法,在调用swap()方法后,你能说出a b的值分别是多少吗?
我们先看在内存中是怎样运行的:
刚刚说过方法调用的机制,可以看到,当执行到swap方法这一行时,会在栈中新开一个空间,
而我们的这几行代码:
public static void swap(int a, int b) {
//这个方法的作用是交换a b的值
int t;
t = a;
a = b;
b = t;
}
则是在这个新开辟的栈空间里执行:
可见,main方法中的a、 b将参数传递给了swap方法空间中的a、b,注意,这两个a、b是在不同的空间的,也就是说,虽然swap方法中和main方法中的两个a的值都是1,但是它们不占有同一个空间,本质上不是一个a。让我们看看接下来会怎样:
swap方法中的a b的值交换,交换过后,swap的方法执行结束,按照我刚刚所说的,swap方法会被栈弹出,从此不复存在:
而我们可以看到,这个交换的过程,跟main方法的a和b是完全没有关系的,所以这个swap方法,交换的参数只是swap方法空间中的形式参数而已,与main方法的a b完全无关。
所以最终main方法中的a b的值,并没有变化。
2、引用类型数据的传递
public class Example04 {
public static void main(String[] args) {
B01 b01 = new B01();
int[] arr = {1,2,3};
System.out.print("更改前 arr = ");
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i]);
}
b01.changeArr(arr);
System.out.println();
System.out.print("更改后 arr = ");
for (int i = 0; i < arr.length; i++) {
System.out.print( + arr[i]);
}
}
}
class B01 {
//在B01类中定义方法,作用是将数组中第一个元素和第二个元素交换
public void changeArr(int[] arr) {
int t;
t = arr[0];
arr[0] = arr[1];
arr[1] = t;
}
}
如图我定义了一段代码,我在B01类中定义方法changeArr(),作用是将数组中第一个元素和第二个元素交换。
在主方法中 我定义一个数组arr = {1,2,3} 并对这个arr数组使用这个changeArr()方法。
请问最后我打印出来的arr数组是{1,2,3}不变,还是{2,1,3}成功交换了呢?
答案是{2,1,3}成功交换了。
因为数组数据是引用类型,就像之前的cat和cat1一样,是通过地址,指向同一个内存区域。
所以更改的时候即便会新开一个栈空间,也会直接将其指向的内存区域进行更改。
如图所示,arr[ ]数组创建了以后,会在堆中开辟一个空间(假定地址是0x1133),这个空间就存放着arr[ ]数组的值。
而我们的arr则指的就是这个地址0x1133,所以我们在传参数给changeArr()方法时,传入的就是堆中数组空间的地址,所以在更改的时候,改变的就是堆中真正的数组空间的数据,也就是会实际影响到真正的数据。
3、小结
new了一个新对象,就是在堆中开辟一个新的对象空间,如果只是单纯的让cat1 = cat,这并不是创建了一个新的对象。
而对于参数而言,一定要确定其类型是否是引用类型。这样才能准确的判断参数的实质改变。
六、方法的重载
(这里跳过了方法递归的内容,留到数据结构与算法再聊)
1、什么是方法的重载?
如图这两个方法就构成了方法的重载:
在同一个类中两个sum方法的传入的参数不同,会构成方法重载,这避免了给众多的方法起名字的麻烦。(就不用再取名叫sum1、sum2……直接统一用sum即可)
2、重载注意事项
(1)方法名必须相同,才能构成重载。
(2)形参列表中参数的类型、顺序、数量,这三者必须有一个及以上个不同,才能构成重载。
(3)重载对于返回类型没有要求,如果只有返回类型不同,也不能构成重载。
七、可变参数
在方法中可能添加一个或者多个同类型参数时使用
1、如何使用可变参数?
如图所示:
在这个地方,可变参数的意思就是,可以往这个方法中传递任意多个int型变量,来进行相加。
可以这样一个一个输入,也可以使用数组的形式传递进去。
输出结果都是:
2、可变参数注意事项
(1)可变参数在方法的形参列表中,必须排在最后一个,不然会报错:
(2)在同一个方法中不可以有多个可变参数,只能有一个。
(3)在使用可变参数的时候当做数组来处理即可
(使用for循环处理数组相加)
八、参数的作用域
参数的生死存亡
1、属性/全局变量
如图,在这段代码中定义了一个A04类,内有一个属性i,初始化为1,它的作用范围就是整个类的代码块中。
例如:这里我创建一个方法,让i = 10;这时的i就是类中的属性i,说明只要在这个类中,无论是在这个类中,还是在这个类的方法中,类中的属性都可以使用,因此类的属性也叫全局变量。
2、局部变量
局部变量指在方法或者代码块(代码块的内容以后再聊)中定义的变量。
它的作用范围就是其定义的方法或代码块内部。
例如:我在A04类中创建一个方法,再在方法中定义一些变量,这些变量就都是局部变量
现在我在这个方法外,试图访问方法内的局部变量,
编译器报错,无法识别,这就是因为超过了其作用域。
3、全局变量 局部变量使用细节
(1)默认值
局部变量使用时需要给定一个值,不然无法使用。
而全局变量则有默认值,不需要给定值也可以直接使用。
局部变量没有默认值,所以要先初始化一个值才可以使用。
(2)同名变量的就近原则
如果全局变量和某方法内的局部变量重名,会发生什么呢?
例如,我在这里设置一个属性i = 1;AAA方法内的局部变量 i = 2(注意,这里我是重新定义了一个i,而不是更改属性里的i的值,跟之前有区别,之前没有int 来重新定义,现在有int 是重新定义了一个局部变量i)然后打印出i的值,那么这个i打印出来的是1 还是 2呢?
遇到局部变量和属性(全局变量)同名,就要遵循就近原则。
对于方法中的System.out.println(i)来说,必然是局部变量i更近一些,所以打印出来的也就是2。
结果:
这个就近原则的“近”不是字面意思上的近,例如:
这看似System.out.println(i)这条输出i的语句,距离属性i = 1更近一些,但实际上它们隔着一个方法的大括号的厚障壁,所以还是局部变量i = 2会被打印出来。
(3)修饰符
局部变量不可以加访问修饰符,而属性/全局变量可以加访问修饰符
九、构造器
便于在创建对象时就可以初始化对象
1、使用构造器
如图中代码所示,Cat cat = new Cat();这个地方的Cat()其实就是使用了一个构造器,只不过是一个无参构造器:
public class Example01 {
public static void main(String[] args) {
Cat cat = new Cat();
}
}
class Cat{
int age;
String name;
}
我们也可以自己定义一个构造器。如:
构造器的最开头是一个访问修饰符,然后是类名,最后是参数列表,跟方法不同的地方在于它没有返回值。(名字也必须与类名保持一致)
这是我们定义的一个有参构造器,可以将你传入的age name 直接放到创建的对象cat中的属性 age name中:
public class Example01 {
public static void main(String[] args) {
Cat cat = new Cat(3, "小白");
}
}
class Cat{
int age;
String name;
public Cat(int age, String name) {
this.age = age;
this.name = name;
}
}
这样我们通过这条语句:Cat cat = new Cat(3, "小白");
直接将3和“小白”按顺序带入即可在创建对象的时候初始化对象,
就可以让对象cat的age = 3,name = “小白”。非常方便。
2、构造器使用细节
(1)我们新建一个类时,这个类的内部是自带一个隐藏的无参构造器的。
这也是为什么我们没有自己定义构造器的时候,也可以使用无参构造器来创建对象。
(2)当我们自己定义一个有参构造器时,类自带的无参构造器会自动清除,此时如果想再使用无参构造器,就要显式的自己重新定义一下。
(是的,构造器也可以有多个,构造器的重载)
例如:
我们重新定义一下无参构造器:
(3)创建对象时,自动调用构造器中的方法
在这里我在无参构造器中写一个方法,来证明:
使用无参构造器创建cat对象:
结果:
所以可见
创建对象时,会自动调用构造器中的方法。
(4)构造器完成的是对象的初始化,并不是创建对象。
如图所示,构造器只是在对象创建之后,将其中的属性赋予相应的值,并不参与对象创建(对象空间在堆中的空间开辟)这个过程本身。
十、this关键字
this就是这个对象本身
1、this的作用
在使用有参构造器时,可以看到this关键字:
我们可以看到形参列表里的age和Cat的属性age是重名的,为了让编译器明白到底age是哪个age,我们用this关键字 + “.” + age 来代表这是对象的属性age。其中this就是对象本身。
所以对于cat对象而言,cat.age和this.age其实就是一回事。
对于cat对象,this.age 等价于 cat.age
2、 this在内存中的存在形式
跟属性一样,在堆中的对象本体中存储,this中存储的就是对象本身的地址,如图:
可见,this.age 和cat.age 对于cat而言是等价的,因为指向的都是同一个对象。
只不过this是对象创建时就自带的。
3、this的使用细节
this只能在类的方法中使用,不能在类的外部使用。
(显然,如果在main方法中使用this,那谁知道this是cat还是dog呢?所以只能在Cat类或者Dog类的定义内部使用)
就先整理这些吧。废了蛮多功夫的,参考了一些资料,希望对各位有帮助!
后续会继续更新面向对象的笔记,还请持续关注!