【Java SE(二)】面向对象:类、封装、初始化、继承、内部类、抽象类、多态、Object类、代理

文章目录

  • 一、类与对象
    • 1.对象的三个主要特性
    • 2.类之间的关系
    • 3.高内聚、低耦合
  • 二、类的定义与使用
    • 1.对象与对象变量
    • 2.隐式参数与显式参数
    • 3.封装
    • 4.私有方法
    • 5.方法重载
    • 6.域的初始化
      • (1)使用无参构造器
      • (2)显式域初始化
      • (3)初始化块
    • 7.this关键字
    • 8.包
  • 三、继承
    • 1.类、超类、子类
    • 2.强制类型转换
    • 3.抽象类
    • 4.Object类
      • (1)equals方法
        • Object类的equals方法
        • 许多的类会重写equals方法
        • null和null的相等问题
      • (2)·····hashCode方法(待补充)
      • (3)·····toString方法(待补充)
    • 5.泛型数组列表ArrayList
      • 访问数组列表元素
  • 四、内部类
    • 1.内部类与外部类
    • 2.局部内部类
    • 3.匿名内部类
    • 4.静态内部类
    • 5.代理
      • (1)静态代理
      • (2)动态代理
      • (3)Cglib代理

一、类与对象

  类是抽象的,对象是具体的。

1.对象的三个主要特性

  对象的行为、对象的状态、对象标识。

2.类之间的关系

  在类之间最常见的关系有:依赖(uses-a)、聚合(has-a)、继承(is-a)。在满足需求的情况下,我们应该尽可能地将类之间的联系减至最少,让类之间的耦合度最小。

3.高内聚、低耦合

  内聚关注模块内部的元素结合程度,耦合关注模块之间的依赖程度。

  • 低耦合:一个完整的系统,模块与模块之间,尽可能的使其独立存在。也就是说,让每个模块,尽可能的独立完成某个特定的子功能。模块与模块之间的接口,尽量的少而简单。如果某两个模块间的关系比较复杂的话,最好首先考虑进一步的模块划分,这样有利于修改和组合 。
  • 高内聚:指一个软件模块是由相关性很强的代码组成,只负责一项任务,也就是常说的单一责任原则。

  并不是内聚越高越好,耦合越低越好,真正好的设计是在高内聚和低耦合间进行平衡,也就是说高内聚和低耦合是冲突的。
  最强的内聚莫过于一个类只写一个函数,这样内聚性绝对是最高的。但这会带来一个明显的问题:类的数量急剧增多,这样就导致了其它类的耦合特别多,于是整个设计就变成了“高内聚高耦合”了。由于高耦合,整个系统变动同样非常频繁。
  对于耦合来说,最弱的耦合是一个类将所有的函数都包含了,这样类完全不依赖其它类,耦合性是最低的。但这样会带来一个明显的问题:内聚性很低,于是整个设计就变成了“低耦合低内聚”了。由于低内聚,整个类的变动同样非常频繁。
  真正做到高内聚、低耦合是很难的,很多时候未必一定要这样,更多的时候“最适合”的才是最好的。

  降低耦合度的方法 :

  • 少使用类的继承,多用接口,隐藏实现的细节。
  • 模块的功能化分尽可能的单一。
  • 多用设计模式,比如采用MVC的设计模式就可以降低界面与业务逻辑的耦合度。

二、类的定义与使用

1.对象与对象变量

  • 一个对象变量并没有实际包含一个对象,而仅仅引用一个对象。
  • 局部变量不会自动地初始化为null,而必须通过调用new或将它们设置为null进行初始化。可以显式地将对象变量设置为null,表明这个对象变量目前没有引用任何对象。如果将一个方法应用于一个值为null的对象上,那么就会产生运行错误。

2.隐式参数与显式参数

  隐式参数不会出现在方法的声明里,在方法中最好加this.加以区分强调。

3.封装

  封装不过是将数据和行为组合在一个包中,并对对象的使用者隐藏了数据的实现方式。实现封装的关键在于绝对不能让类的实例域直接被其他类访问到,而是通过对象的方法与对象数据进行交互。
  非常需要注意的一点是,不要编写返回引用可变对象的访问器方法:

	class Employee
	{
		private Date hireDay;
		……
		public Date getHireDay()
		{
			return hireDay;
		}
	}
	//这样破坏了类的封装性
	Employee harry=……;
	Date d=harry.getHireDay();
	double tenYearsInMilliSeconds=10*365*24*60*60*1000;
	d.setTime(d.getTime-(long)tenYearsInMilliSeconds);
	// let's give Harry ten years of added seniority

  出错的原因很很妙,d和harry.hireDay引用同一个对象(请参见下图)。对d调用set方法就可以自动地改变这个雇员对象的私有状态。
【Java SE(二)】面向对象:类、封装、初始化、继承、内部类、抽象类、多态、Object类、代理_第1张图片
  如果需要返回一个可变对象的引用,应该首先对它进行克隆(clone)。对象克隆是指存放在另一个位置上的对象副本,修改后的代码如下:

	class Employee(){
		……
		public Date getHireDay()
		{
			return hireDay.clone()}
	}

4.私有方法

  有时,可能希望将一个计算代码划分成若干个独立的辅助方法。通常,这些辅助方法不应该成为公有接口的一部分,这是由于它们往往与当前的实现机制非常紧密,或者需要一个特别的协议以及一个特别的调用次序。最好将这样的方法设计为private的。这些方法不为外部可见,设计者可以随意删除、修改。

5.方法重载

  相同的名字、不同的参数。
  返回类型不是方法签名的一部分。也就是说,不能有两个名字相同、参数类型也相同却返回不同类型值的方法。

6.域的初始化

  人为地在类中对域进行初始化是非常明智的,为每个属性指定一个有意义的初值,是一个非常好的编程习惯。

(1)使用无参构造器

  只有当设计者没有人为地设计任何构造器时,系统才会默认创建一个无参构造器;如果设计者没有设计无参构造器,就不能使用无参构造器来实例化对象(因为类中不存在这样的无参构造器)。
  为了实现域的默认初始化,可以这样写构造器:

	public Employee(){
		this.name ="";
		this.salary=0;
		this.hireDay=new Date();
	}

(2)显式域初始化

  在执行构造器之前,先执行赋值操作。当一个类的所有构造器都希望把相同的值赋予某个特定的实例域时,这种方式特别有用,如下:

	public Employee(){
		private String name="";
	}

  初始值不一定是常量。在下面的例子中,可以调用方法对域进行初始化。和Mysql的主键自增一样,仔细看一下Employee类,其中每个雇员有一个id域。可以使用下列方式进行初始化:

	class Employee(){
		private static int nextId:
		private int id=assignId();
		……
		private static int assignId()
		{
			int r = nextId;
			nextId++;
			return r;
		}
	}

(3)初始化块

  初始化块会在构造方法执行之前被调用,此外还有static初始化块,也可以用于实现主键自增功能。

7.this关键字

  除了用来调用属性之外,还可以用来在一个构造器中调用类中的其他构造器,不过这个语句必须写在方法体的第一行,如下:

	public Employee(double s)
	{
	this("Employee #"+nextId,s);
	nextId++;
	}

8.包

  使用包的主要原因是确保类名的唯一性,以及把功能类似的类放在同一个包下,便于使用和管理。
  需要注意的是,嵌套的包之间没有任何关系。

  类之间的访问:
  一个类可以使用所属包中的所有类,以及其他包中的公有类。访问其他包中的公有类的方式如下:

  ①在每个类名之前添加完整的包名java.util.Date today=new java.util.Date();
  ②使用import语句:在package语句之后写import语句,最好写清楚导入的具体的类,以避免类名同名带来的问题。
  此外,可以使用静态导入(import static java.lang.System.*;),以导入类的静态方法和静态域而不必加类的前缀(out.println("hello");)。不过,可读性有时是一个问题。

三、继承

1.类、超类、子类

  • 访问父类方法,super.方法名();
  • 访问父类构造器,super(参数列表);,必须放在方法体的第一句。

2.强制类型转换

  • 只能在继承层次内进行类型转换。
  • 在将超类转换成子类之前,应该使用instanceof进行检査。

  一般情况下,少使用类型转换和instanceof,更多的是检查一下类是否设计不合理。

3.抽象类

  • 抽象类不能实例化。
  • 尽量将通用的方法和域放在父类中。
  • 类即使不含抽象方法,也可以将类声明为abtract。
  • 多态:可以定义一个抽象类的对象变量,但是它只能引用非抽象子类的对象。

4.Object类

  Object类是所有类的超类,可以使用Object类的变量引用任何类型的对象。除了基本类型,所有的类、数组类型,不管是对象数组还是基本类型的数组都扩展于Object类。

(1)equals方法

Object类的equals方法

  equals方法在Object类中检测一个对象与另一个对象的引用是否相同,如果两个对象的引用是相同的,那么它们一定是相同的(也就是和==的作用一样)。但事实上,我们往往需要验证两个对象的状态是否相同就行,但即使两个对象的状态相同,他们的引用也未必相同。所以,许多子类一般会重写equals方法,使得它们状态相同就返回true。

许多的类会重写equals方法

  先看这段代码,这是一般的重写equals方法的思路:

	class Employee{
		...
		public boolean equals(Object otherObject)
		{
			//1.这是最完美的情况,两个对象的引用相等,也就是说指向同一个对象
			if(this==otherObject)return true;
			
			//2.如果两个对象有且仅有一个是null,返回false
			//这里只检测otherObject是因为Employee对象如果为null,这个equals方法调用时会报错
			//而能进行到这里说明此对象不可能为null
			if(otherObject==null)return false;
			
			//3.如果两个对象属于不同类,返回false,因为它们的状态不可能相等
			if(this.getClass()!=otherObject.getClass())return false;
			
			//4.以上情况都不是,说明它们没有null,又属于同一个类(在继承层次内),但引用并不相等
			//这时候需要比对两个对象的状态
			Employee other=(Employee)otherObject;
			return Object.equals(this.name,other.name);
				&&this.salary==other.salary;
				&&Object.equasl(this.hireDay,other.hireDay);
		}
	}

null和null的相等问题

  我有一个问题是,如何判断null和null是否相等,看下面一段代码:

	boolean result1 = null.equals(null);//无法执行,报错
	boolean result2 = null==null;//返回true

  第一句根本无法运行,是因为null对象根本没有equals方法,因为equals方法源于Object类以及它的子类,显然null不在其中,因此根本无法调用方法。
  第二句返回true,说明null和null的引用是相同的,那就是:都没有引用。

(2)·····hashCode方法(待补充)

(3)·····toString方法(待补充)

5.泛型数组列表ArrayList

  参数类型只能是Object类(引用类型)。
  另外,常见的方法如下:
【Java SE(二)】面向对象:类、封装、初始化、继承、内部类、抽象类、多态、Object类、代理_第2张图片

访问数组列表元素

  泛型数组严格意义上来说仍是数组,所以如果有较多的插入、删除中间元素的操作,使用链表会比较合适。
  先声明一个泛型数组列表staff:

	ArrayList<Emloyee> staff=new ArrayList<Emloyee>(100);
	Employee[] a=new Employee[100]();

  常用方法如下:
  ①set方法:设置(替换)第i个元素的值
  staff.set(i,value);等价于a[i]=value;
  需要注意的是,只有当i小于或等于数组列表的大小时,才能够调用set方法,比如下面的代码就是错误的:

	ArrayList<Emloyee> staff=new ArrayList<Emloyee>(100);
	staff.set(0,harry);

  这是因为,staff的容量虽然为100,但size却为0,所以不存在下标为0的元素。
  ②add方法:添加新元素,或在指定位置插入元素
  ③get方法:获得元素
  需要注意的是,泛型数组只能返回参数类型的对象,没有指定泛型的数组列表将返回Object对象。所以,必要的时候需要用到类型转化。
  ④toArray方法:转化为静态数组
  ⑤remove方法:删除尾元素,或者指定位置的元素

四、内部类

1.内部类与外部类

  • 内部类可访问外部类的状态,内部类是外部类的私有类。
  • 内部类访问外部类属性:OuterName.this.属性名;创建内部类对象:外部对象.new 内部类();在外部类作用域外引用内部类:外部类名.内部类名

2.局部内部类

  即在方法中定义的类,它对外部完全隐藏,除了此方法没人知道局部内部类的存在。局部类不能用public或private访问说明符进行声明。它的作用域被限定在声明这个局部类的块中。

3.匿名内部类

  假如只需要对一个类创建一个对象,就没必要创建一个类了,这种类被称为匿名内部类。格式如下:

	//SuperClass是一个类
	new SuperClass(构造参数){
		内部类的方法和参数
		内部类需要扩展SupersClass,比如重写方法
	}
	//InterfaceType是一个接口
	new InterfaceType(){
		内部类的方法和参数
		内部类需要实现InterfaceType
	}

  下面看看构造一个普通类对象与构造一个匿名对象的区别:

	//构造一个Person类对象
	Person queen=new Person("Mary");
	//构造一个继承自Person类的匿名内部类
	Person count=new Person("Lucy"){
	};

4.静态内部类

  为了避免内部类与其他类重名或被他人访问,可以将内部类使用static关键字定义为静态内部类,通过外部类名.内部类名的形式访问它。需要注意的是,静态内部类不能访问外部类对象,这会破坏外部类的封装性。
  声明在接口中的内部类自动成为public和static。

5.代理

  代理(Proxy)是一种设计模式,提供了对目标对象另外的访问方式,即通过代理对象访问目标对象。这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能。这里使用到编程中的一个思想:不要随意去修改别人已经写好的代码或者方法,如果需改修改,可以通过代理的方式来扩展该方法。
  代理模式的关键点是:代理对象与目标对象.代理对象是对目标对象的扩展,并会调用目标对象。

(1)静态代理

  静态代理在使用时,需要定义接口或者父类,被代理对象与代理对象一起实现相同的接口或者是继承相同父类。静态代理类调用的时候通过调用代理对象的方法来调用目标对象。代码如下:

	//定义接口
	public interface IUserDao {
	    void save();
	}
	
	//定义目标对象
	public class UserDao implements IUserDao {
	    public void save() {
	        System.out.println("----已经保存数据!----");
	    }
	}
	
	//定义静态代理对象
	public class UserDaoProxy implements IUserDao{
	    //接收保存目标对象
	    private IUserDao target;
	    public UserDaoProxy(IUserDao target){
	        this.target=target;
	    }
	
	    public void save() {
	        System.out.println("开始事务...");
	        target.save();//执行目标对象的方法
	        System.out.println("提交事务...");
	    }
	}
	
	//测试类
	public class App {
	    public static void main(String[] args) {
	        //目标对象
	        UserDao target = new UserDao();
	        //代理对象,把目标对象传给代理对象,建立代理关系
	        UserDaoProxy proxy = new UserDaoProxy(target);
	        proxy.save();//执行的是代理的方法
	    }
	}

  静态代理的优点是:可以做到在不修改目标对象的功能前提下,对目标功能扩展。
  静态代理的缺点是:因为代理对象需要与目标对象实现一样的接口,所以会有很多代理类,类太多。同时,一旦接口增加方法,目标对象与代理对象都要维护。
  为解决静态代理中的缺点,可以使用动态代理方式。

(2)动态代理

  主要特点就是能够在程序运行时JVM才为被代理对象生成代理对象。常说的动态代理也叫做JDK代理也是一种接口代理,JDK中生成代理对象的代理类就是Proxy,所在包是java.lang.reflect。
  代理对象不需要实现接口,但是目标对象一定要实现接口,否则不能使用动态代理,因此这也算是这种方式的缺陷。
  具体的代码实现,没搞懂,以后再说。

(3)Cglib代理

  上面的静态代理和动态代理模式有个相同点就是都要求目标对象是实现一个接口的对象,然而并不是任何对象都会实现一个接口,也存在没有实现任何的接口的对象,这时就可以使用继承目标类以目标对象子类的方式实现代理,这种方法就叫做Cglib代理,也叫作子类代理,它是在内存中构建一个子类对象从而实现对目标对象功能的扩展。使用JDK动态代理有一个限制,就是被代理的对象必须实现一个或多个接口,若想代理没有实现接口的类,就需要使用Cglib实现。

你可能感兴趣的:(Java后端开发校招)