七.类与对象
1.引用
1.引用与指向
new Hero(); /* 代表创建了一个Hero对象 但是也仅仅是创建了一个对象,没有办法访问它 为了访问这个对象,会使用引用来代表这个对象 */ Hero h = new Hero(); /* h这个变量是Hero类型,又叫做引用 =的意思指的h这个引用代表右侧创建的对象 “代表” 在面向对象里,又叫做“指向” */
2.多个引用,一个对象
引用有多个,但是对象只有一个。 在这个例子里,所有引用都指向了同一个对象。 对象就像 "房产", 引用就像"房产证" 房产证的复印件可以有多张,但是真正的"房产" 只有这么一处
[图片上传失败...(image-e3e5a9-1585128289044)]
3.一个引用,多个对象
引用garen指向新创建的对象(对象1) 同一个引用garen指向新创建的对象(对象2) 这个时候,对象1,就没有任何引用指向了 换句话说,就没有任何手段控制和访问该对象,那么该对象就变得没有意义
[图片上传失败...(image-300e3-1585128289044)]
2.方法重载
含义: 方法的重载指的是方法名一样,但是参数类型不一样
1.attack方法的重载
有一种英雄,叫做物理攻击英雄 ADHero 为ADHero 提供三种方法 public void attack() public void attack(Hero h1) public void attack(Hero h1, Hero h2) 方法名是一样的,但是参数类型不一样 在调用方法attack的时候,会根据传递的参数类型以及数量,自动调用对应的方法
public class ADHero extends Hero { public void attack() { System.out.println(name + " 进行了一次攻击 ,但是不确定打中谁了"); } public void attack(Hero h1) { System.out.println(name + "对" + h1.name + "进行了一次攻击 "); } public void attack(Hero h1, Hero h2) { System.out.println(name + "同时对" + h1.name + "和" + h2.name + "进行了攻击 "); } public static void main(String[] args) { ADHero bh = new ADHero(); bh.name = "赏金猎人"; Hero h1 = new Hero(); h1.name = "盖伦"; Hero h2 = new Hero(); h2.name = "提莫"; bh.attack(h1); bh.attack(h1, h2); } }
2.可变数量的参数
如果要攻击更多的英雄,就需要设计更多的方法,这样类会显得很累赘,像这样: public void attack(Hero h1) public void attack(Hero h1,Hero h2) public void attack(Hero h1,Hero h2,Hero h3) 这时,可以采用可变数量的参数 只需要设计一个方法 public void attack(Hero ...heros) 即可代表上述所有的方法了 在方法里,使用操作数组的方式处理参数heros即可
public class ADHero extends Hero { public void attack() { System.out.println(name + " 进行了一次攻击 ,但是不确定打中谁了"); } // 可变数量的参数 public void attack(Hero... heros) { for (int i = 0; i < heros.length; i++) { System.out.println(name + " 攻击了 " + heros[i].name); } } public static void main(String[] args) { ADHero bh = new ADHero(); bh.name = "赏金猎人"; Hero h1 = new Hero(); h1.name = "盖伦"; Hero h2 = new Hero(); h2.name = "提莫"; bh.attack(h1); bh.attack(h1, h2); } }
3.构造方法
通过一个类创建一个对象,这个过程叫做实例化
实例化是通过调用构造方法(又叫做构造器)实现的
1.什么是构造方法
方法名和类名一样(包括大小写) 没有返回类型 实例化一个对象的时候,必然调用构造方法
2.隐式的构造方法
Hero类的构造方法是 public Hero(){ } 这个无参的构造方法,如果不写,就会默认提供一个
3.如果提供了一个有参的构造方法
一旦提供了一个有参的构造方法 同时又没有显式的提供一个无参的构造方法 那么默认的无参的构造方法,就“木有了“
public class Hero { String name; //姓名 float hp; //血量 float armor; //护甲 int moveSpeed; //移动速度 //有参的构造方法 //默认的无参的构造方法就失效了 public Hero(String heroname){ name = heroname; } public static void main(String[] args) { Hero garen = new Hero("盖伦"); Hero teemo = new Hero(); //无参的构造方法“木有了” } }
4.构造方法的重载
和普通方法一样,构造方法也可以重载
public class Hero { String name; //姓名 float hp; //血量 float armor; //护甲 int moveSpeed; //移动速 //带一个参数的构造方法 public Hero(String heroname){ name = heroname; } //带两个参数的构造方法 public Hero(String heroname,float herohp){ name = heroname; hp = herohp; } public static void main(String[] args) { Hero garen = new Hero("盖伦"); Hero teemo = new Hero("提莫",383); } }
4.this关键字
this这个关键字,相当于普通话里的“我” 小明说 “我吃了” 这个时候,“我” 代表小明 小红说 “我吃了” 这个时候,“我” 代表小红 "我"代表当前人物 this这个关键字,相当于普通话里的“我” this即代表当前对象
1.this代表当前对象
public class Hero { String name; //姓名 float hp; //血量 float armor; //护甲 int moveSpeed; //移动速度 //打印内存中的虚拟地址 public void showAddressInMemory(){ System.out.println("打印this看到的虚拟地址:"+this); } public static void main(String[] args) { Hero garen = new Hero(); garen.name = "盖伦"; //直接打印对象,会显示该对象在内存中的虚拟地址 //格式:Hero@c17164 c17164即虚拟地址,每次执行,得到的地址不一定一样 System.out.println("打印对象看到的虚拟地址:"+garen); //调用showAddressInMemory,打印该对象的this,显示相同的虚拟地址 garen.showAddressInMemory(); Hero teemo = new Hero(); teemo.name = "提莫"; System.out.println("打印对象看到的虚拟地址:"+teemo); teemo.showAddressInMemory(); } }
2.通过this访问属性
/* 通过this关键字访问对象的属性*/ public class Hero { String name; //姓名 float hp; //血量 float armor; //护甲 int moveSpeed; //移动速度 //参数名和属性名一样 //在方法体中,只能访问到参数name public void setName1(String name){ name = name; } //为了避免setName1中的问题,参数名不得不使用其他变量名 public void setName2(String heroName){ name = heroName; } //通过this访问属性 public void setName3(String name){ //name代表的是参数name //this.name代表的是属性name this.name = name; } public static void main(String[] args) { Hero h =new Hero(); h.setName1("teemo"); System.out.println(h.name); h.setName2("garen"); System.out.println(h.name); h.setName3("死歌"); System.out.println(h.name); } }
3.通过this调用其他的构造方法
/* 如果要在一个构造方法中,调用另一个构造方法,可以使用this() 一个多参数构造方法可以调少参数构造方法 */ public class Hero { String name; //姓名 float hp; //血量 float armor; //护甲 int moveSpeed; //移动速度 //带一个参数的构造方法 public Hero(String name){ System.out.println("一个参数的构造方法"); this.name = name; } //带两个参数的构造方法 public Hero(String name,float hp){ this(name); System.out.println("两个参数的构造方法"); this.hp = hp; } public static void main(String[] args) { Hero teemo = new Hero("提莫",383); System.out.println(teemo.name); // 此时也默认调用了上一个构造方法 } }
5.传参
1.基本类型传参
基本类型传参 在方法内,无法修改方法外的基本类型参数
2.引用与=
如果一个变量是基本类型 比如 int hp = 50; 我们就直接管hp叫变量 =表示赋值的意思。 如果一个变量是类类型 比如 Hero h = new Hero(); 我们就管h叫做引用。 =不再是赋值的意思 =表示指向的意思 比如 Hero h = new Hero(); 这句话的意思是 引用h,指向一个Hero对象
3.类类型传参
类类型又叫引用 第24行的引用 teemo与 第17行的引用hero,是不同的引用 通过调用garen.attack(teemo, 100); 使得这两个引用都指向了同一个对象 所以在第18行hero.hp = hero.hp - damage; 就使得该对象的hp值,发生了变化 因此第25行,打印该对象的Hp值就是变化后的值
[图片上传失败...(image-7826ea-1585128289045)]
public class Hero { String name; // 姓名 float hp; // 血量 float armor; // 护甲 int moveSpeed; // 移动速 public Hero(String name, float hp) { this.name = name; this.hp = hp; } // 攻击一个英雄,并让他掉damage点血 public void attack(Hero hero, int damage) { hero.hp = hero.hp - damage; } public static void main(String[] args) { Hero teemo = new Hero("提莫", 383); Hero garen = new Hero("盖伦", 616); garen.attack(teemo, 100); System.out.println(teemo.hp); } }
6.访问修饰符
成员变量有四种修饰符 private 私有的 package/friendly/default 不写 protected 受保护的 public 公共的 那么什么情况该用什么修饰符呢? 从作用域来看,public能够使用所有的情况。 但是大家在工作的时候,又不会真正全部都使用public,那么到底什么情况该用什么修饰符呢? 1. 属性通常使用private封装起来 2. 方法一般使用public用于被调用 3. 会被子类继承的方法,通常使用protected 4. package用的不多,一般新手会用package,因为还不知道有修饰符这个东西 再就是作用范围最小原则 简单说,能用private就用private,不行就放大一级,用package,再不行就用protected,最后用public。 这样就能把数据尽量的封装起来,没有必要露出来的,就不用露出来了
[图片上传失败...(image-1373a-1585128289045)]
7.类属性
当一个属性被static修饰的时候,就叫做类属性,又叫做静态属性
当一个属性被声明成类属性,那么所有的对象,都共享一个值
与对象属性对比:
不同对象的 对象属性 的值都可能不一样。
比如盖伦的hp 和 提莫的hp 是不一样的。
但是所有对象的类属性的值,都是一样的
类属性
/* 类属性: 又叫做静态属性 对象属性: 又叫实例属性,非静态属性 如果一个属性声明成类属性,那么所有的对象,都共享这么一个值 给英雄设置一个类属性叫做“版权" (copyright), 无论有多少个具体的英雄,所有的英雄的版权都属于 Riot Games公司。 */ public class Hero { public String name; //实例属性,对象属性,非静态属性 protected float hp; static String copyright;//类属性,静态属性 public static void main(String[] args) { Hero garen = new Hero(); garen.name = "盖伦"; Hero.copyright = "版权由Riot Games公司所有"; System.out.println(garen.name); System.out.println(garen.copyright); Hero teemo = new Hero(); teemo.name = "提莫"; System.out.println(teemo.name); System.out.println(teemo.copyright); } }
访问类属性
访问类属性有两种方式 1. 对象.类属性 teemo.copyright 2. 类.类属性 Hero.copyright 这两种方式都可以访问类属性,访问即修改和获取,但是建议使用第二种 类.类属性 的方式进行,这样更符合语义上的理解
什么时候使用对象,属性什么时候使用类属性
如果一个属性,每个英雄都不一样,比如name,这样的属性就应该设计为对象属性,因为它是跟着对象走的,每个对象的name都是不同的 如果一个属性,所有的英雄都共享,都是一样的,那么就应该设计为类属性。比如血量上限,所有的英雄的血量上限都是 9999,不会因为英雄不同,而取不同的值。 这样的属性,就适合设计为类属性
通过garen.copyright修改其值 garen.copyright = "Blizzard Entertainment Enterprise"; 然后打印teemo.copyright,观察是否有变化 (可以更改)
8.类方法
类方法: 又叫做静态方法 对象方法: 又叫实例方法,非静态方法 访问一个对象方法,必须建立在有一个对象的前提的基础上 访问类方法,不需要对象的存在,直接就访问
1.类方法
/* 类方法: 又叫做静态方法 对象方法: 又叫实例方法,非静态方法 访问一个对象方法,必须建立在有一个对象的前提的基础上 访问类方法,不需要对象的存在,直接就访问 注意: 实例化对象既可以调用类方法,也可以调用对象方法 类名只能调用类方法 */ public class Hero { public String name; protected float hp; //实例方法,对象方法,非静态方法 //必须有对象才能够调用 public void die(){ hp = 0; } //类方法,静态方法 //通过类就可以直接调用 public static void battleWin(){ System.out.println("battle win"); } public static void main(String[] args) { Hero garen = new Hero(); garen.name = "盖伦"; //必须有一个对象才能调用 garen.die(); Hero teemo = new Hero(); teemo.name = "提莫"; //无需对象,直接通过类调用 Hero.battleWin(); } }
2.调用类方法
和访问类属性一样,调用类方法也有两种方式 1. 对象.类方法 garen.battleWin(); 2. 类.类方法 Hero.battleWin(); 这两种方式都可以调用类方法,但是建议使用第二种 类.类方法 的方式进行,这样更符合语义上的理解。 并且在很多时候,并没有实例,比如在前面练习的时候用到的随机数的获取办法 Math.random() random()就是一个类方法,直接通过类Math进行调用,并没有一个Math的实例存在。
3.什么时候设计对象方法,什么时候设计类方法
如果在某一个方法里,调用了对象属性,比如 public String getName(){ return name; } name属性是对象属性,只有存在一个具体对象的时候,name才有意义。 如果方法里访问了对象属性,那么这个方法,就必须设计为对象方法 如果一个方法,没有调用任何对象属性,那么就可以考虑设计为类方法,比如 public static void printGameDuration(){ System.out.println("已经玩了10分50秒"); } printGameDuration 打印当前玩了多长时间了,不和某一个具体的英雄关联起来,所有的英雄都是一样的。 这样的方法,更带有功能性色彩 就像取随机数一样,random()是一个功能用途的方法 Math.random()
4.练习
/* 在一个类方法中,直接调用一个对象方法, 比如在battleWin中调用die() 能否直接调用? 为什么? */ 答:不能,静态方法不能调用非静态方法和非静态变量
9.属性初始化
1.对象属性初始化
/* 对象属性初始化有3种: 1. 声明该属性的时候初始化 2. 构造方法中初始化 3. 初始化块 */ public class Hero { public String name = "some hero"; //声明该属性的时候初始化 protected float hp; float maxHP; { maxHP = 200; //初始化块 } public Hero(){ hp = 100; //构造方法中初始化 } } /* 对象属性的初始化有三种方式 故意把初始化块,放在构造方法下面,问题: 这三种方式,谁先执行?谁后执行? 答:属性声明->初始化块->构造方法*/
2.类属性初始化
/* 类属性初始化有2种 1. 声明该属性的时候初始化 2. 静态初始化块 */ public class Hero { public String name; protected float hp; float maxHP; //物品栏的容量 public static int itemCapacity=8; //声明的时候 初始化 static{ itemCapacity = 6;//静态初始化块 初始化 } public Hero(){ } public static void main(String[] args) { System.out.println(Hero.itemCapacity); } }
注意: 1.对象的初始化顺序:(1)类加载之后,按从上到下(从父类到子类)执行被static修饰的语句;(2)当static语句执行完之后,再执行main方法;(3)如果有语句new了自身的对象,将从上到下执行构造代码块、构造器(两者可以说绑定在一起)。 2.类属性不能在构造器中初始化, 加载一个类的时候, 类属性是在创建这个类的对象之前就已经初始化了, 而实例属性是在创建该类的对象的时候才执行初始化,
10.单例模式
该类,只能被实例化一次
1.单例模式
单例模式又叫做 Singleton模式,指的是一个类,在一个JVM里,只有一个实例存在。
2.饿汉式单例模式
GiantDragon 应该只有一只,通过私有化其构造方法,使得外部无法通过new 得到新的实例。 GiantDragon 提供了一个public static的getInstance方法,外部调用者通过该方法获取12行定义的对象,而且每一次都是获取同一个对象。 从而达到单例的目的。 这种单例模式又叫做饿汉式单例模式,无论如何都会创建一个实例
public class GiantDragon { //私有化构造方法使得该类无法在外部通过new 进行实例化 private GiantDragon(){ } //准备一个类属性,指向一个实例化对象。 因为是类属性,所以只有一个 private static GiantDragon instance = new GiantDragon(); //public static 方法,提供给调用者获取12行定义的对象 public static GiantDragon getInstance(){ return instance; } } public class TestGiantDragon { public static void main(String[] args) { //通过new实例化会报错 // GiantDragon g = new GiantDragon(); //只能通过getInstance得到对象 GiantDragon g1 = GiantDragon.getInstance(); GiantDragon g2 = GiantDragon.getInstance(); GiantDragon g3 = GiantDragon.getInstance(); //都是同一个对象 System.out.println(g1==g2); System.out.println(g1==g3); } }
3.懒汉式单例模式
懒汉式单例模式与饿汉式单例模式不同,只有在调用getInstance的时候,才会创建实例
public class GiantDragon { //私有化构造方法使得该类无法在外部通过new 进行实例化 private GiantDragon(){ } //准备一个类属性,用于指向一个实例化对象,但是暂时指向null private static GiantDragon instance; //public static 方法,返回实例对象 public static GiantDragon getInstance(){ //第一次访问的时候,发现instance没有指向任何对象,这时实例化一个对象 if(null==instance){ instance = new GiantDragon(); } //返回 instance指向的对象 return instance; } } public class TestGiantDragon { public static void main(String[] args) { //通过new实例化会报错 // GiantDragon g = new GiantDragon(); //只能通过getInstance得到对象 GiantDragon g1 = GiantDragon.getInstance(); GiantDragon g2 = GiantDragon.getInstance(); GiantDragon g3 = GiantDragon.getInstance(); //都是同一个对象 System.out.println(g1==g2); System.out.println(g1==g3); } }
4.什么时候使用饿汉和懒汉
/* 饿汉式是立即加载的方式,无论是否会用到这个对象,都会加载。 如果在构造方法里写了性能消耗较大,占时较久的代码,比如建立与数据库的连接,那么就会在启动的时候感觉稍微有些卡顿。 懒汉式,是延迟加载的方式,只有使用的时候才会加载。 并且有线程安全的考量(鉴于同学们学习的进度,暂时不对线程的章节做展开)。 使用懒汉式,在启动的时候,会感觉到比饿汉式略快,因为并没有做对象的实例化。 但是在第一次调用的时候,会进行实例化操作,感觉上就略慢。 看业务需求,如果业务上允许有比较充分的启动和初始化时间,就使用饿汉式,否则就使用懒汉式*/
5.单例模式三元素
这个是面试的时候经常会考的点,面试题通常的问法是: 什么是单例模式? 回答的时候,要答到三元素 1. 构造方法私有化 2. 静态属性指向实例 3. public static的 getInstance方法,返回第二步的静态属性
11.枚举
1.预先定义的常量
枚举enum是一种特殊的类(还是类),使用枚举可以很方便的定义常量 比如设计一个枚举类型 季节,里面有4种常量 public enum Season { SPRING,SUMMER,AUTUMN,WINTER } 一个常用的场合就是switch语句中,使用枚举来进行判断 注:因为是常量,所以一般都是全大写
public class HelloWorld { public static void main(String[] args) { Season season = Season.SPRING; switch (season) { case SPRING: System.out.println("春天"); break; case SUMMER: System.out.println("夏天"); break; case AUTUMN: System.out.println("秋天"); break; case WINTER: System.out.println("冬天"); break; } } } public enum Season { SPRING,SUMMER,AUTUMN,WINTER }
2.使用枚举的好处
假设在使用switch的时候,不是使用枚举,而是使用int,而int的取值范围就不只是1-4,有可能取一个超出1-4之间的值,这样判断结果就似是而非了。(因为只有4个季节) 但是使用枚举,就能把范围死死的限定在这四个当中 SPRING,SUMMER,AUTUMN,WINTER 而不会出现奇怪的 第5季
public class HelloWorld { public static void main(String[] args) { int season = 5; switch (season) { case 1: System.out.println("春天"); break; case 2: System.out.println("夏天"); break; case 3: System.out.println("秋天"); break; case 4: System.out.println("冬天"); break; } } }
3.遍历枚举
/*借助增强型for循环,可以很方便的遍历一个枚举都有哪些常量 */ public class HelloWorld { public static void main(String[] args) { for (Season s : Season.values()) { System.out.println(s); } } }
4.练习
public class Hero{ public enum HeroType { TANK,WIZARD,ASSASSIN,ASSIST,WARRIOR,RANGED,PUSH,FARMING } public static void main (String[] args) { for (HeroType type:HeroType.values())//增强型for循环 { switch (type) { case TANK: System.out.println("坦克"); break; case WIZARD: System.out.println("法师"); break; case ASSASSIN: System.out.println("刺客"); break; case ASSIST: System.out.println("辅助"); break; case WARRIOR: System.out.println("近战"); break; case RANGED: System.out.println("远程"); break; case PUSH: System.out.println("推进"); break; case FARMING: System.out.println("打野"); break; } } } }