目录
一、类与对象
1、基本概念和定义
场景:比如要进行同样的操作,比如都是加法。要对100组数进行相加。一个个单独写加法没有必要,不利于数据的管理,而且效率低,所以就可以专门来写一个方法,来形成统一的操作。
java设计者引入类与对象(OOP编程),根本原因是基础的奇数,不能完美解决新的需求。
- 一个程序就是一个世界,有很多事物(即:对象[属性,行为])比如:小狗[年龄,吃饭]
- 类:int这种是java系统提供的数据类型,像下面“猫”这种定义的类,就是自定义的数据类型。
- 对象(实例):int这个数据类型可以对应很多数,比如10,20。同样“猫”这个类,也可以对应很多个不同的猫,这就称为对象(或者叫实例)。通过“猫”类,可以创建猫对象,即具体的猫,可以有无数个。
- 有很多不同的说法:创建一个对象 or 实例化一个对象 or 把类实例化……
所以把一种事物的共性提取出来,形成了“类”这个自定义数据类型(至少包含属性和行为),有了类以后,就可以创建很多具体的猫。
JAVA最大的特点就是面向对象。
public class Object01 {
public static void main(String[] args){
// 可以实例化一只猫(创建一个猫对象)
Cat cat1 = new Cat();
cat1.name = "小白";
cat1.age = 3;
cat1.color = "白色";
Cat cat2 = new Cat(); // 这样就创建了第二只猫
cat1.name = "小花";
cat1.age = 5;
cat1.color = "花色";
System.out println("第一只猫的信息" + cat1.name + cat1.age + cat1.color);
System.out println("第二只猫的信息" + cat2.name + cat2.age + cat2.color);
}
}
// 首先定义一个“猫”类
class Cat{
String name; // 属性:名字
int age; // 属性:年龄
String color; // 属性:颜色
}
2、对象在内存中的存在形式(重要)
2.1 内存存在形式
- 1、在new创建的时候,首先会把类的信息加载到“方法区”,包含1、属性信息 2、行为(方法信息)。然后会在堆中分配空间,进行默认初始化。
- 2、创建实例之后,在栈里面有一个cat,对应一个地址。该地址指向的堆里面有三个小空间(因为cat有3个属性),cat是对象名(也叫对象引用),实际的对象存放在堆和方法区里面。
- 3、第一个空间name因为是String类型,所以会指向方法区中的常量池的一个空间,具体内容是“小白”。(color也是String类型的,即一个引用类型,所以也是还要放到方法区的常量池)
- 3、第二个空间age是int基本数据类型,所以直接存在堆的小空间里面。
- 也可以先申明Cat cat1 (只有一个名字); 再创建对象 cat = new Cat();(这里才开辟空间)
2.2 内存分配形式
比如下面,把对象p1赋给对象p2。会把p1的地址拷贝给p2,即p2也指向堆中的相同地方。相当于一个东西有两个名字,这俩可以共同访问和更改同样的对象。所以下面p2.age=10
3、属性和成员变量
- 属性properties:年龄、颜色、名字也可以叫成员变量(甚至也有人叫field字段)。
- 属性一般是基本数据类型,也可以是引用类型(比如数组、另外一个对象)
- 属性的定义:访问修饰符 属性类型 属性名;(访问修饰符:是用来控制属性的访问范围。有四种:public,protected,默认,private 后面会着重讲这个访问修饰符)
- 属性如果不赋值,会有默认值,规则和数组一致。
二、成员方法
1、方法简介和小案例
成员方法简称方法。比如人类,除了“年龄、姓名”这种属性,还有一些行为,比如“说话、跑步”
public class Object02 {
public static void main(String[] args){
Person p1 = new Person(); //先创建实例
p1.speak(); // 这里才是调用了方法,
// 会去找Peron这个类里面的speak方法,然后执行代码体。
p1.cal01();
p1.cal02(5); // 调用cal02方法,传入值5
p1.cal02(15); // 可以多次调用这个方法
int sum = p1.getSum(5,2); // 传入两个值,用sum来接收
System.out println("getSum方法返回的值是:"+ sum); // 就可以打印输出
}
}
// 可以在Person类里面添加成员方法
class Person{
String name;
int age;
//public表示这个方法是公开的,void表示没有返回值,speak是方法名, ()是形参列表
public void speak(){ // {}中的方法体,写具体要执行的代码
System.out println("我是一个好人");
}
// 添加一个方法,可以计算从1到100的和
public void cal01(){
int res = 0;
for(int i=0;i<=1000;i++){
res+=i;
}
System.out println("计算结果:"+ res);
}
// 添加一个方法,可以接收一个数n,计算从1到n的结果
public void cal02(int n){ // 表示当前有一个形参n,可以接收用户的输入
int res = 0;
for(int i=0;i<=n;i++){
res += i;
}
System.out println("计算结果:"+ res);
}
// 添加一个方法,可以计算两个数的和
public int getSum(int num1, int num2){ // int表示该方法执行后有一个int类型的返回值
int res = num1 + num2;
return res; // 表示返回res的值,有接收就返回,没有接收就不返回
}
}
2、方法调用机制原理(内存)
1、第一句话先实例化p1,在栈中一个对象名p1,指向堆中的对象空间
2、第二句话,p1 = getSum(10,20); 会在栈中再开辟一个独立的空间。然后把10和20赋值给num1和num2,计算出res并返回(可以认为返回之后,这个计算的栈空间就被释放了)。把结果赋值给res,然后第三句话print打印。
- 当程序执行到方法时,就会开辟一个独立的栈空间。
- 当方法执行完毕,或者执行到return语句,就会返回。
- 返回到调用方法的地方,继续执行后面的代码。当main栈执行完毕后,整个程序就退出。
3、小细节
- 方法的优势:1、提高代码的复用性 2、可以将实现的细节封装起来,其他用户来调用即可。
- 如果是void,那么可以不写return,或者只写一个retun,不能有具体返回值。但是如果有返回数据类型如int,就必须有return,可以return具体的常量、或者是表达式。而且要求返回类型必须一致或者兼容。
- 访问修饰符可以不写,那就是默认的。 直接void speak(){}
- 一个方法最多有一个返回值,如果要返回多个数值,可以用数组来实现。然后用arr[0] arr[1]来访问具体的数值。
- 方法名最好用驼峰命名法。并且最好有一定的含义。
- 调用带参数的方法时,必须传入相同类型或者兼容类型的参数。
- 方法定义的时候的参数是形参,调用方法时候传入的参数是实参。形参和实参的顺序也要一致
- 方法体里面不能再定义方法(方法不能嵌套定义)。
- 一个类中的方法可以互相直接调用。但是跨类的方法要互相调用,也要先创建对象,再调用。要用对象名.方法名。(注意:方法的跨类调用,和访问修饰符有关)
三、成员方法传参机制(重要)
1、基本、引用数据类型传参的不同
基本数据类型,传递的是值(值拷贝),所以形参的任何改变不影响实参。
public class MtehodParameter01 {
public static void main(String[] args){
int a = 10;
int b = 20;
AA aa = new AA();
aa.swap(a,b); // 调用swap这个方法,传入a和b进行交换,这里面调用方法后会输出20、10
System.out println("a="+a+"b="+b); // 这一步会输出10、20(原本ab的值)
}
}
class AA{
public void swap(int a, int b){
int temp = a;
a = b;
b = temp; // 交换ab
System.out println("a="+a+"b="+b);
}
}
引用数据类型,传递的是地址,所以在方法中对形参修改,也会影响实参。
public class MtehodParameter02 {
public static void main(String[] args){
int[] a = {1,2,3};
BB bb = new BB();
bb.test(a);
System.out println("main中的输出:"+arr[0]);
// 这一步会输出200(引用数据类型,是地址拷贝,所以实参的修改也对该地址中的数据进行了修改)
}
}
class BB{ // 接收一个数组并修改
public void test(int[] arr){
arr[0] = 200;
System.out println("test中的输出:"+arr[0]);
}
}
传对象也是一样的,但是注意!下面的两个变化,如果在方法中重新实例化对象,那么就会在堆中重新创建一个对象空间,即!!形参p指向新的对象空间,而实参指向旧的对象空间。
public class MtehodParameter02 {
public static void main(String[] args){
Person p = new Person();
p.name = 'jack';
p.age = 10;
BB b = new BB();
b.test(p); // 传入的是p,一个对象
System.out println(p.age);
// 引用类型,形参影响实参,输出100
}
}
class Person{
String name;
int age;
}
class BB{ // 接收一个数组并修改
public void test(Person p){
p.age = 100; // 栈中形参也指向了堆中的同一个地址。
// 变化1:p = null ; // 如果是这样的话,main中p.age会输出10
// 这并不会把堆重点对象空间置空,只是栈中的test空间里面的p不再指向任何空间了
// 变化2:
// p = new Person;
// p.name = "tom";
// p.age = 99;
// 现在main方法中输出10.因为test方法中重新new了p,即会指向堆中的一个新的对象空间
//(而不是指向实参对应的那个对象空间)
}
}
2、克隆对象
要求克隆的和原始对象相互独立,但是要有相同的属性
public class MtehodParameter03 {
public static void main(String[] args){
Person p = new Person();
p.name = 'jack';
p.age = 10;
// 实例化克隆的方法
MyTools tools = new MyTools();
Person copyp = tools.copy01(p); // 调用方法,克隆p,返回一个新对象
// 新对象和原始对象是相互独立的,只是属性相同。(可以通过输出对象的哈希code来看对象是否是一个)
System.out.println(p == p2); // 返回false
}
}
class Person{
String name;
int age;
}
class MyTools{ // 该方法可以克隆对象(复制person对象)
public Person copy01(Person p){ // 注意返回的数据类型是Person
// 创建一个新对象,并复制属性
Person p2 = new Person();
p2.name = p.name;
p2.age = p.age;
return p2;
}
}
四、方法递归调用
1、机制原理
递归就是方法自己调用自己,每次调用时传入不同的变量,有助于解决复杂问题,让代码变简洁。
递归Recursion的应用场景有很多,比如8皇后问题、汉诺塔、阶乘问题、迷宫问题。各种算法里面也用的很多,比如快排、归并排序、二分查找、分治计算等等。
- 对于下面这个打印的递归调用,在类的方法中实现递归思路。每次调用test()方法,都会重新开一个栈,等于说会开3个栈空间,然后直到if条件不成立,才会执行到打印2。
- 蓝色的线:最底层的栈执行完了之后返回,会返回到倒数第二层的栈,打印3,然后再回到倒数第3个栈,打印4。最后才返回到主栈。所以输出的顺序是2、3、4.
- 每一个栈都会完整执行所有方法。左边是打印,右边这个例子是阶乘:
// 阶乘案例
public class Recursion {
public static void main(String[] args){
T t1 = new T();
int res = t1.factorial(5); //
}
}
class T{
public int factorial(int n){
if(n == 1){
return 1;
}else{
return factorial(n-1) * n;
// 会额外开辟5个栈。然后再从底层往外层调用
// 最底层的栈执行到if结束,然后其余4层都会执行f()*xx
}
}
}
注意:假设递归传的是引用数据类型,那么多次递归就会相互影响。
递归必须向退出递归的条件逼近,否则就是无限递归,出现StackOverflowError
2、经典案例
要返回运用当前方法输出的结果时,就用递归。
1)编写斐波那契数列1,1,2,3,4,8
public class Recursion {
public static void main(String[] args){
T t1 = new T();
int res = t1.fibonacci(7);
}
}
class T{
public int fibonacci(int n){
if(n == 1 || n == 2){
return 1;
}else{
return fibonacci(n-1) + fibonacci(n-2);
}
}
}
2)迷宫问题
要从左上角走到右下角。把下面这个迷宫看成8行7列的数组。用0表示可以走的路1表示不能走的
下面这个代码用递归实现,根据策略,相当于先递归一直往下走,能走通的标2,直到走不通了,再向右走。。。再往上走,再往左走。直到map[6][5]这个位置被标位2
当然这个找路策略也可以改变(不一定非得下右上左)
public class Migong {
public static void main(String[] args){
// 先来画迷宫地图
int[][] map = new int[8][7];
for(int i=0;i<7;i++){
map[0][i] = 1; // 最上面和最下面一行为1
map[7][i] = 1;
}
for(int i=0;i<8;i++){
map[i][0] = 1; // 最左边和最右边一列为1
map[i][7] = 1;
}
map[3][1] = 1;
map[3][2] = 1;
System.out.println("初始地图为:")
for(int i=0;i<map.length;i++){
for(int j=0;j<map[i].length;j++){
System.out.print(map[i][j]+" ");
}
System.out.println(); // 换行
}
T t1 = new T();
t1.findway(map,1,1;); // 因为数组为引用类型,所以在方法中的修改,对于实参t也有影响,直接打印输出即可
System.out.println("找到路了:")
for(int i=0;i<map.length;i++){
for(int j=0;j<map[i].length;j++){
System.out.print(map[i][j]+" ");
}
System.out.println();
}
}
}
class T{
public boolean findway(int[][] map, int i, int j){ // ij表示初始位置
// 递归找路。用0表示可以走、1表示障碍物、2表示可以走、3表示已经走过但是是死路
// 当map[6][5]的值为2的时候,就表明找到通路了,否则继续找。
// 确定老鼠找路的策略 下-右-上-左
if(map[6][5] == 2){
return true; // 已经找到了(没走到的都为0)
}else{
if(map[i][j] = 0){ // 当前位置为0,可以走
map[i][j] = 2; // 先设为2
// 然后用找路策略来看能不能真的走通
if(findway(map,i+1,j)){ // 看下面位置
return true;
}else if(findway(map,i,j+1)){ // 看右边位置
return true;
}else if(findway(map,i-1,j)){ // 看上边位置
return true;
}else if(findway(map,i,j-1)){ // 看右边位置
return true;
}else{
map[i][j] = 3; // 如果上下左右都为1,都走不通,把当前值设为3 是死路,返回false
return false;
}
}else{
return false;
}
}
}
}
回溯:比如要是在某一个位置上下左右都走不通了,就会回到上一个位置,即回到上一个栈。
3)汉诺塔问题
就是要把A塔上的盘移到C塔。要求永远都必须大盘在下,小盘在上。
把所有盘,都看成最下面一个盘和上面一堆盘(简化问题)
public class HanoiTower {
public static void main(String[] args){
Tower tower = new Tower();
tower.move(3,'A','B','C');
}
}
class Tower{
// move这个方法表示把num个盘,从a塔移动到c塔,借助b
public void move(int num, char a, char b, char c){
if(num == 1){ // 如果只有1个盘,那么直接从a放到c
System.out.println(a + "->" + c);
}else{
// 如果有多个盘,也先看成两个盘(最下面的,和上面的所有盘num-1)
move(num-1,a,c,b); // 借助c,把上面所有的盘移到b
System.out.println(a + "->" + c); // 把最下面的这个盘移动到c
move(num-1,b,a,c); // 借助a,把上面所有的盘从b移动到c
}
}
}
五、方法重载overload
java中允许同一个类中,多个同名方法的存在,但要求形参列表不一致(类型or顺序or个数不一样 都可以)。不然就会报错“重复定义方法”。(返回类型不要求,仅返回类型不一样也是重复定义)
比如System.out.println( ) 这个方法可以传入不同的值。其实在out这个对象下面有很多个名为“println()”的方法,但是他们的形参列表不一样println(int a); println(char a)……
好处:减轻了起名的麻烦、有利于接口编程。
如下,下面这个类里面实现了3个方法的重载(方法名一样,形参列表不同)。在main中实例化对象调用后,传入值,根据传入数值的类型进行匹配,程序会自己看应该具体调用哪个方法。
public class MyMethod{
public static void main(String[] args){
Mycalculator AA = new Mycalculator();
AA.calculate(3,4);
AA.calculate(3,4.0);
AA.calculate(3.0,4);
}
}
class Mycalculator{
public int calculate(int n1, int n2){
return n1+n2;
}
public double calculate(int n1, double n2){
return n1+n2;
}
public double calculate(double n2, int n1){
return n1+n2;
}
}
如果方法是(double,double,double),传入的是(double,double,int),那么也可以把int自动转化为double,从而匹配到这个方法。但是要是有(double,double,int)这个方法,那优先级更高。
- 可以自动类型转换,但还是优先匹配最为相符的。
六、可变参数
- java允许将同一个类中多个同名同功能但参数个数不同的方法,封装成一个方法。就可以通过可变参数实现。语法: 访问修饰符 返回类型 方法名(数据类型…形参名){ }
- 可以把参数当做一个数组。
- 可变参数可以和普通参数一起放在形参列表,但必须保证可变参数在最后。一个形参列表中只能出现一个可变参数。
public class MyMethod{
public static void main(String[] args){
Method AA = new Method();
int result = AA.sum(1,2,3);
}
}
// 方法 求2个数的和、3个数的和……,可以不用每个都写一个方法
class Method{
public int sum(int... nums){ // 表示可以接收多个int参数,可以吧nums当作数组
System.out println("接收的参数个数=" + nums.length);
// 求和
int res = 0;
for(int i=0; i<nums.length; i++){
res += nums[i];
}
return res;
}
}
七、变量作用域(重要)
- 在java编程中,主要的变量就是属性(成员变量)和局部变量。
- 局部变量一般是指在成员方法中定义的变量(比如上面代码中的res)。局部变量的作用域只在这个方法中。
- 成员变量在类中定义(方法外),在成员方法中都可以使用。除了属性之外的全部变量,可以认为是局部变量。(比如写在代码块里面的变量即使在方法之外,但是也不算成员变量)
- 属性可以不赋值,直接使用,因为有默认值。但是局部变量必须赋值后才能使用。
class Cat{
int age = 10;
double weight; // 不赋值,也会有一个默认值0.0
public void hi(){
int num; // 局部变量,不会给默认值。
System.out println(num); // 会报错,没有初始化变量
}
}
细节:
- 属性和局部变量是可以重名的,使用的时候遵循就近原则。
- 同一个作用域中,两个局部变量不能重名。
- 属性的生命周期较长,伴随着对象的创建而创建,对象的销毁而销毁。局部变量的生命周期较短,方法执行完了之后就销毁了。
- 属性能被本类内部或者其他对象调用使用。局部变量只能在本类的本方法中使用。
- 属性可以加修饰符,但是局部变量不能加修饰符。
八、构造器(构造方法)
需求场景:比如之前要先创建一个Person对象,再给他的年龄和姓名属性赋值。现在想在创建对象的时候就直接制定他的年龄和名字。
constructor构造器: 是类的一种特殊方法,它主要作用是完成对新对象的初始化。并不是创建对象,只是初始化对象: [修饰符] 方法名(形参列表){ 方法体;}
- 构造器没有返回值。方法名和类名必须一样。参数列表必须和成员方法一样的规则。
- 构造器的调用,由系统完成。即new的时候,就自动调用构造器给属性赋值。
public class Constructor01 {
public static void main(String[] args){
Person p1 = new Person("jack",80);
// new一个对象时,直接通过构造器来给属性赋值
}
}
class Person{
int age; // 本来默认为0
String name;
// 下面这个就是构造器Person(名字和类名一样)
public Person(String pname, int page){ // 形参列表规则和成员变量一样
name = pname; // 这里就初始化属性了
age = page;
}
}
细节:
- 一个类可以定义多个构造器,为构造器的重载。(比如可以再有一个构造器只初始化name)
- 如果没有定义构造器,系统会给类自动生成一个默认无参构造器。(可以用javap反编译看)一旦定义了自己的构造器,默认的构造器就被覆盖了,new Person()就会报错。除非再显式定义下Person(){}才能直接用。
- 比如自己定义无参构造器,方法中可以写age=18.这样new Person()之后,age都初始18.
构造器的流程:加载类信息(只会加载一次)——在堆中分配空间——默认初始化——显式初始化——对象的初始化(看有没有自定义的构造器)——把对象在堆中的地址返回给栈中的p对象名
九、关键字this
想让构造器的形参直接写成属性名。不再用写pname、page。但是name=name,age=age的时候,程序不知道name是属性还是形参(根据就近原则,两个等式左右的name都是局部变量形参了)
- java虚拟机会给每个对象分配this,代表当前对象。
- 原代码中用this.name可以表示当前对象的属性。所以就可以用this.name = name;
- 可以认为创建对象的时候,就也有一个隐藏的属性this,指向对象本身。
可以用哈希code(把该对象的实际地址转化成一个整数)来看。调用哪个对象,this就指向谁。
public class This01 {
public static void main(String[] args){
Person p1 = new Person("jack",20);
System.out println("p1的hashcode的值为:" + p1.hashCode()); // 打印hashcode,相当于地址
}
}
class Person{
int age;
String name;
public Person(String name, int age){
this.name = pname; // 用this表示该对象的属性
this.age = page;
System.out println("this的hashcode的值为:" + this.hashCode()); // 这和main中打印的结果是一样的
}
}
- this关键字可以用来访问本类的属性、方法、构造器。
- this访问成员方法的语法: this.方法名(参数列表)
- this还能访问构造器: this(参数列表) 注意只能在构造器中调用另一个构造器。
// 判断一个对象和另一个对象属性是否相同,相同则返回true
public class This02 {
public static void main(String[] args){
Person p1 = new Person("mary",20);
Person p1 = new Person("jack",30);
p1.compareTo(p2); // 比较当前对象p1和传进去的p2是否相同
}
}
class Person{
int age;
String name;
public Person(String name, int age){
this.name = pname;
this.age = page;
}
public boolean compareTo(Person p){
return this.name.equals(p.name) && this.age == p.age;
}
}