Java类的创建与分类:从对象实例到抽象类与接口的完整解析
发布时间:2026/9/26 8:06:42来源:尧图网络
1. 一个类和对象到底是什么先建立正确的画面感说实话刚开始学Java的时候很多人都会被“类是模板对象是实例”这种话给绕晕。我当年也是这样老师讲得清清楚楚课本写得明明白白但一打开IDE就是写不出来。后来我发现了问题的根源我们的大脑是面向“具体事物”运行的而不是面向“抽象概念”运行的。所以你先别去背那句“类是对事物的抽象”先用画面感把它固定下来。你想象这样一个场景你是一个做蛋糕的师傅手头有一份“巧克力蛋糕配方”。这份配方上面写着需要鸡蛋几个、面粉多少克、烤多久、什么温度、用什么模具。配方本身不能吃但你拿着配方照着步骤去做就能烤出一个又一个实实在在的、可以摆在橱窗里的蛋糕。配方就是“类”蛋糕就是“对象”。每一次照着配方做出来的蛋糕都是独立的——你今天烤的那个和明天的那个虽然长得一样但它们是两回事谁也不会替代谁。这个画面感一旦建立类体系里最核心的语法就都好解释了class关键字就是你在写“配方”的封面告诉别人这是一份类定义。构造方法Constructor就是你烤蛋糕时启动烤箱的那个动作——每次执行就产生一个全新的对象。new关键字就是你“照着配方动手做”的那一下在Java里叫“实例化”。对象的属性字段就是配方上写的“加多少糖、多少奶油”这些参数每个蛋糕可以有自己的值。对象的方法行为就是“切一块下来尝尝”这种你可以对蛋糕做的事。我见过很多初学者对着Person p new Person();这一行发呆完全不知道发生了什么。拆开来看的话Person()是对构造方法的调用new负责在内存里分配一块空间并执行这个构造方法p则是你贴在蛋糕盒子上的一张标签通过它你才能找到这个蛋糕。等号左边和右边做了完全不同的事这一点后面内存分析时会非常关键。这篇博文我们聊的就是Java类的创建与分类。说白了就是让你搞清楚类的代码结构怎么组织才能写起来顺手、读起来舒服一个类从写出来到加载进内存再到变成对象中间经历了什么普通类、抽象类、内部类、匿名类这些分类到底是怎么回事面试里高频出现的那些“类”相关八股题背后的原理是什么。如果你正在准备Java面试或者刚学完Java基础语法但写代码还是没什么手感这篇文章应该能帮你在“类”这条主线上补上不少盲区。建议你打开IDE跟着敲光看是记不牢的。2. 手写一个实用类的完整过程字段、构造器、方法之间的协作关系很多人写类喜欢一上来就堆字段然后点生成Setter/Getter完事。这种写法不能说错但你完全没体会到“类作为模板”的设计乐趣。写类本质上是在设计一份“约束说明书”哪些信息是对象必须有的必填字段哪些是可以有默认值的可选字段哪些信息外部可以改提供Setter哪些只能看不能动只读字段对象一出生就要做什么构造方法对象在这个过程中会有什么行为普通方法。2.1 第一次写类从字段到构造方法我先带你把一个真正能用的类写出来不是那种只打印hello world的Demo而是能在实际逻辑里站得住脚的类。我们来做一个非常常见的需求会员系统中的用户实体。public class Member { // 字段所有 Member 对象都会带上的数据 private final String memberId; // 会员编号一旦创建不可修改 private String nickname; // 昵称允许修改 private int level; // 等级默认 1 private long points; // 积分默认 0 // 构造方法对象诞生的入口 public Member(String memberId, String nickname) { this.memberId memberId; this.nickname nickname; this.level 1; this.points 0L; } // 行为方法描述这个对象能做什么 public void addPoints(long delta) { if (delta 0) { throw new IllegalArgumentException(增加的积分必须为正数); } this.points delta; } public boolean canUpgrade() { return this.points 1000L this.level 10; } public void upgrade() { if (canUpgrade()) { this.level; this.points - 1000L; } } }你别小看这个类它里面藏了挺多值得玩味的细节。你注意看memberId这个字段我说了它“一旦创建不可修改”所以给了final而且没有提供Setter。这是非常重要的一个设计习惯有些字段是根据业务规则不应该变的比如订单号、身份证号、会员ID你就要用final把它们锁死而不是默认一切字段都可写。很多后面写糟糕代码的人就是从“懒得分清楚哪些字段可变、哪些不可变”开始的。另外你看addPoints方法我特意检查了入参是否为正数。这是很多人新手期完全不会在意的点——他们写完this.points delta;就觉得完事了。但你想如果调用方传了一个负数进来那用户积分被扣了你还不知道去哪查这种bug特别难找。一个成熟的类不是字段最全而是把约束装在类里面不允许外部随便破坏它的内部状态。2.2 构造方法的重载同一个类多种出生方式实际业务里同一个实体经常有不同的“出生场景”。举个例子管理员后台手动创建一个新会员只知道他的手机号其余信息待完善用户自己注册一次性填了手机号、昵称和性别运营批量导入了历史会员这些人已经有积分和等级了。这三种场景如果全靠同一个构造方法写起来会非常痛苦——你每次都要从调用方那边把所有参数凑齐凑不齐就得传null。所以Java允许你定义多个构造方法这叫“构造方法重载”。public class Member { private String memberId; private String nickname; private String phone; private int level; private long points; // 场景一只知道手机号其他给默认值 public Member(String memberId, String phone) { this(memberId, phone, 新会员, 1, 0L); } // 场景二完整信息 public Member(String memberId, String phone, String nickname, int level, long points) { this.memberId memberId; this.phone phone; this.nickname nickname; this.level level; this.points points; } // 场景三从老系统导入ID格式不同 public Member(String legacyId, boolean fromLegacy) { this.memberId L legacyId; this.phone ; this.nickname 待完善; this.level 1; this.points 0L; } }这里有个比较专业的小技巧你看第二段方法里面的this(...)方法调用。一个构造方法可以调用同一个类的另一个构造方法用来避免字段初始化代码的重复。注意它有个硬性要求this(...)必须是构造方法的第一行。这是Java语法规定的——否则编译器没法保证“先初始化后做其他事”。面试的时候有个比较细的考点构造方法能不能被final修饰答案是不能。因为构造方法本来用途就是创建对象不存在“被子类重写”的语义final加在它身上没有意义编译器会直接报错。你如果去翻开发规范很多团队会要求类的字段要么在声明处初始化、要么在构造方法里初始化不要写一个“半初始化”的类——比如字段声明了但没给默认值构造方法里也只给一部分字段赋值。这种类很容易出现“对象已经存在了但某些字段还没来得及有值”的尴尬状态一调用就NPE空指针异常。2.3 方法不只是“函数”它是给外部世界的行为接口字段定义的是“这个对象是什么”方法定义的是“这个对象能干什么”。很多人写类把方法当成纯粹的代码容器往里堆一堆工具逻辑这是误解。一个设计良好的类的方法应该是围绕“这个对象的自身行为”去设计的。你看前面我写的canUpgrade和upgrade这两个方法。它们联合起来表达的是“会员可以升级”这个业务能力。外部调用方不需要知道升级的具体规则——不用知道1000分升一级、最高10级这些细节他想给会员升级只需要调用member.upgrade()。至于会员积分不够那方法内部自己会判断不做任何事就完了。这就是封装的意义把规则锁在类里面外面的人用起来省心日后改规则只需要改这一个类。我自己写代码的时候有个习惯公开方法尽量让别人“一看名字就知道干什么”而且不要让人家关心方法内部的执行细节。比如addPoints加了一堆校验逻辑调用方看到的名字依然是“加积分”他不会觉得有什么负担。反过来如果你把方法写成updatePointsWithValidationAndLogging这种命名本身就说明你没想清楚职责——这个方法干了太多事一定会在后续的需求变更里出问题。2.4 static成员属于类而不是属于对象的东西前面说的字段、方法都是“属于对象”的——你得先new一个对象才能访问它们。但还有一种成员它属于类本身和对象无关这就是static。static成员一句话总结从属于类在内存里只有一份所有对象共享它。它的典型使用场景有这么几个工具方法比如Math.max()这种纯计算、不依赖任何对象状态的。全局配置比如“系统当前允许的最大会员数”这属于系统级别的数据不应该每个会员对象都带一份。工厂方法后面我会展开讲用static方法返回对象实例是一种非常经典的设计。面试里有个高频题是“Static方法能不能访问非static成员”答案是不能。道理很简单static方法是属于类的类加载的时候它就已经存在了而此时可能一个对象都还没有创建出来。非static成员必须依赖对象才能存在你让一个“类级别的方法”去访问“对象级别的数据”它去哪儿找对象呢反过来就可以——非static方法可以访问static成员因为对象一定存在于类加载之后。有个比较经典的坑有些人写“单例模式”的时候喜欢在static方法里new自己的类。这种操作不是不行但要处理好多线程并发的问题后面讲内部类的时候我会再提到。3. 类的加载过程详解从源码到对象的中间三条命很多初学者到现在都有一个错误的直觉写完Member.java之后new Member()就是直接照着源码创建对象。实际上远没有这么简单一个类从“磁盘上的Java文件”到“内存里实实在在的对象”中间要经历编译、加载、连接、初始化、实例化好几个阶段。每一个阶段出问题都会以不同的异常形态出现在你的日志里不懂这些阶段的人排查起来就像无头苍蝇。3.1 加载字节码文件进入内存首先Member.java会被javac编译成Member.class这个字节码文件它躺在硬盘上什么也不干。当你第一次真正用到这个类的时候JVM的类加载器ClassLoader会把这份字节码读进内存生成对应的Class对象。注意这里有个反直觉的点即便是你自己写的类它在JVM里也是以一个对象的形式存在的只不过这个对象描述的是“类本身”而不是具体的某个会员。什么时候会触发类加载不是程序一启动就全部加载那样太浪费内存了。JVM采用懒惰策略触发的时机包括遇到new指令实例化对象时调用静态方法或访问静态字段时用反射访问类信息时初始化某个类的子类时子类触发了父类也得跟着加载。所以你可以观察到这样一个现象一个类即使有毛病只要你从头到尾没用它程序就能正常跑。只要哪一行代码首次触发到它噼里啪啦的异常才出来。3.2 连接验证、准备、解析紧跟着加载的是一系列“连接”操作。验证很好理解——JVM会检查这个字节码文件是不是结构合法、有没有违反安全规则。毕竟字节码文件不一定是javac生成的理论上你可以手写一个但为了JVM的安全它得把关。准备阶段有个面试爱问的细节这个阶段会为静态变量分配内存并设置默认的零值。比如你的类里有static int count;在这个阶段count已经是0了但还没执行你写的“ 10”这种赋值语句。换句话说静态变量的初始化分两步走准备阶段给零值初始化阶段才真正赋你写的那个值。解析阶段则比较复杂大致是把类里的符号引用替换为直接引用。你不用太深究只需要知道如果没有这一步程序运行时的效率会非常难看。符号引用相当于你写文章时“见图1”直接引用相当于编辑器帮你把图1的位置页码标好了一翻就到。3.3 初始化静态代码块和静态字段的舞台到了初始化阶段JVM才开始真正执行类里写的静态变量赋值语句和静态代码块。这个阶段的执行顺序是严格按照代码从上到下的顺序来的public class Config { static int a 5; static { a 10; b 20; // 合法可以给后面声明的静态变量赋值 } static int b; }上面这段代码执行完a是10b是20。但如果你在静态代码块里写System.out.println(b);然后执行你会看到0——因为这行代码执行的时候b还没被显式赋值用的是准备阶段留下的零值。变量可以“先使用后声明”但那时候它的值还是默认值这个细节很容易在笔试里被设计成陷阱题。关于初始化还有一个面试重量级问题new一个子类对象的时候初始化顺序是什么给你一个标准答案父类静态代码块/静态变量按代码顺序→ 子类静态代码块/静态变量 → 父类非静态代码块/非静态变量 → 父类构造方法 → 子类非静态代码块/非静态变量 → 子类构造方法。你可以亲手实验一下打印日志就会看得清清楚楚。这里面的原理就是子类的初始化依赖于父类所以父类必须先被初始化完整之后才能开始子类的部分。你反过来想如果父类还没初始好子类拿什么去继承呢3.4 实例化真正创建对象的过程前面那些阶段都发生在“类”这个层面到了new Member(...)这一步才是真正创建一个具体的成员对象。JVM会在堆内存中为这个对象分配一块空间把字段的内存准备好然后执行你写的构造方法里的代码。构造方法执行完之后p这个引用才算真正指向一个完整的、可以用的对象。这个过程你要记住一个词引用。Java里根本没有“对象变量”这个东西你写的Member p只是“指向Member对象的一个引用变量”。它是对象的一张名片、一个遥控器。你把p赋值给qMember q p;没有产生新对象只是又做了一张指向同一块内存的标签。两个标签操作同一个对象一个改了积分另一个再拿到的就是新值。这个点对后面理解对象比较、参数传递都有影响。4. 类的完整分类地图普通类、抽象类、最终类、内部类类的“分类”这个词在Java里可以从很多个维度去切。有的人说的分类是“public类和默认权限类”有的人说的是“抽象类和普通类”还有人说的是“内部类和外部类”。这篇博文里我按面试和实际开发中最常用的维度给你把地图画出来每个分类背后都在解决一个具体问题。4.1 按是否可以被继承普通类、抽象类、最终类普通类就是能被实例化也能被继承的类。抽象类abstract class是不能被实例化的类它存在的意义是“被继承”。最终类final class反过来它是能被实例化但不能被继承的类典型代表就是String。为什么要用final禁掉继承因为有些类的设计者希望它的行为和内部实现绝对稳定不允许后人通过继承去篡改。String类的设计就是这样——它内部有缓存、池化、不可变特性等一系列复杂的约定一旦允许你继承并重写它的方法整个String体系都可能被破坏。还有java.lang.System这种工具门面也没有必要被别人继承去改写。抽象类有一个比较讲究的设计点它可以有构造方法但这个构造方法不是用来创建自己的而是给子类做初始化用的。你看下面这个例子public abstract class Account { protected String accountId; protected String ownerName; public Account(String accountId, String ownerName) { this.accountId accountId; this.ownerName ownerName; } // 抽象方法子类必须实现 public abstract double calculateInterest(); } public class SavingAccount extends Account { private double balance; public SavingAccount(String accountId, String ownerName, double balance) { super(accountId, ownerName); this.balance balance; } Override public double calculateInterest() { return this.balance * 0.0035; } }注意SavingAccount的构造方法第一行调用了super(...)这就是通过抽象父类的构造方法把基类字段初始化好。哪怕是抽象方法也就是那些没有方法体的方法它们存在的意义是画出一条“子类必须遵守的规范”你作为账户就得能算出利息至于怎么算那是你子类的事。这种设计在框架代码里非常常见日常开发中如果你发现多个类有“共性结构各自差异”就可以考虑抽一个抽象父类出来。4.2 按定义位置外部类和内部类内部类这个分类非常有意思它的核心动机是有些类本质上是另一个类的附属品单独拎出来没有存在意义。比如“订单”里的“订单项”“引擎”里的“活塞”——你在外部几乎不需要单独创建它们它们应该被“包含”在里面。Java提供了四种内部类的形态面试经常考我一个个拆开讲。4.2.1 成员内部类类里的一个普通成员成员内部类就像类的成员变量一样定义在类的内部、方法的外部。它和外部类的关系是一个成员内部类的对象天生持有一个外部类对象的引用。public class Order { private String orderNo; public class OrderItem { private String skuId; private int quantity; public String getOrderNoFromOuter() { return orderNo; // 直接访问外部类的字段 } } }注意OrderItem的代码里直接写了orderNo看起来它自己没有这个字段但它能拿到。原因就是它内部隐藏着OuterClass.this这个引用通过它可以拿到外层对象的字段。成员内部类不能定义静态成员因为它本身必须依赖外部类对象才能存在不存在“脱离了外部对象的静态数据”这种东西。使用的时候语法也有点绕Order order new Order(A1001); Order.OrderItem item order.new OrderItem();注意order.new这个写法。你不先有一个Order对象就没法创建它的OrderItem。这就很形象地传达了“附属品”这个概念。我实际开发里看到很多人把这种类搞得很复杂其实它最适合的场景是“和外部类强绑定、单独不存在业务意义”的类。除了成员内部类还有静态内部类、局部内部类和匿名内部类我继续往下说。4.2.2 静态内部类摆脱外部对象的“附带类”用static修饰的内部类官方叫“静态嵌套类”。它和成员内部类的根本区别是它不持有外部类对象的引用不依赖外部类实例存在。你可以直接new Outer.Inner()不需要先创建外部类对象。那它和“普通类定义在外面”有什么区别区别在于命名空间和访问权限。静态内部类写在外部类里面能访问外部类的私有静态成员而且在外部类的“势力范围”内别人一看就知道这个类是服务于外部类的。经典例子就是Map.Entry——它是Map接口里的一个静态内部类/接口表示“键值对”这个结构你不单独定义在顶层因为它的存在意义就是为Map服务的。public class Response { private int code; private String message; public static class Body { public Object data; public long timestamp; public Body(Object data) { this.data data; this.timestamp System.currentTimeMillis(); } } }实际项目中我经常用这种模式做接口返回的体结构Response.Body读起来比ResponseBody这种平铺命名清晰得多。静态内部类因为不持有外部引用所以它不能直接访问外部类实例字段只能访问静态字段或自己想办法传入。这一点的面试问法通常是“成员内部类和静态内部类的区别是什么”你答出“持有/不持有外部引用”就是得分点。4.2.3 局部内部类和匿名内部类方法内部的小世界局部内部类定义在方法里作用域仅限于方法内你在外面永远不会知道它的存在。匿名内部类更进一步——连名字都不要了直接new一个抽象类或接口的实现出来。在Java 8之后函数式接口只有一个抽象方法的接口可以用Lambda表达式替代匿名内部类写法更简洁。ListMember memberList getMemberList(); // 传统匿名内部类的写法 memberList.sort(new ComparatorMember() { Override public int compare(Member m1, Member m2) { return Long.compare(m1.getPoints(), m2.getPoints()); } }); // Lambda 写法 memberList.sort((m1, m2) - Long.compare(m1.getPoints(), m2.getPoints()));这两种写法本质上是同一种东西——创建一个实现了Comparator接口的匿名对象。Lambda只是语法糖编译后基本还是生成一个内部类。有个细节要注意匿名内部类或者Lambda可以访问方法里的局部变量但这个变量必须是final或事实上的final赋值后不再改变。原因不算难理解局部变量的生命周期随方法走方法出栈变量就没了而内部类对象可能在方法结束后很久才被使用它要访问的变量其实是被复制到内部类里的一份拷贝。如果那个原变量还会被修改两边就会出现数据不一致。所以Java直接把变量“锁死”——你不能改它就不会出这种分歧。4.3 抽象类和接口面试八股文的高频对比面试里十有八九会问到“抽象类和接口的区别”。如果你只是背答案就太可惜了因为理解这两个东西本质上是理解Java设计者对“子类约束”的两种不同力度。抽象类强调的是“不完整的父类”它已经帮你实现了一部分公共逻辑只是有些步骤还不确定留给子类去填空。它是“是什么”的关系子类和父类本质上属于同一种类型。接口强调的是“纯契约”它不提供任何实现Java 8之后有了默认方法另说只规定“你必须能做什么”。接口更多是“像什么”或“能干什么”的关系。一个类可以实现多个接口但只能继承一个抽象父类这就说明Java在设计上认为“能力”可以叠加“身份”只能有一个。实际选型的时候我个人的判断标准很简单如果几个类之间明显有代码可以复用公共字段、公共实现逻辑而且它们是“同一类事物”的不同版本优先用抽象类如果只是几个不相干的类都需要“支持某一个行为”优先用接口。我给你画一张对照表对比维度抽象类接口继承/实现数量只能单继承可以多实现构造方法可以有没有字段可以有实例字段只能是常量public static final方法抽象方法具体方法JDK8前只能抽象JDK8后有默认方法JDK9后有私有方法语义强“is-a”关系弱“has-capability”关系设计侧重代码复用规范约束能力契约解耦面试时这个问题的加分点在于不要说“抽象类就是有构造方法接口没有”这种纯列举要把设计动机讲出来——抽象类是为了“复用代码并让子类填空”接口是为了“强行建立能力契约”。你甚至可以举一两个实际设计的例子比如模板方法模式用抽象类因为父类要执行算法骨架子类只填步骤而Comparable、Runnable这种适合接口因为完全不关心你怎么实现只要你保证有这个能力。5. 用类构建真实项目骨架三层实战术前面讲了不少概念现在我们来玩点真的。假设你要用Java做一个会员管理的小系统我们不谈具体业务细节只看“类”应该怎么组织。很多自学Java的人学到面向对象就卡住了因为语法都懂但一到设计阶段就不知道怎么下手。我给你的思路是按“实体类、服务类、入口类”三个层次去安排你的类。实体类是“数据的载体”上面我写的Member、Order就是这类——它们描述一个业务对象的状态字段对应数据库的列或前端表单的字段。服务类承载“业务流程和规则”比如MemberService负责注册、充值、升级这种操作逻辑。入口类负责“与外界打交道”比如MainController接收用户指令然后调用服务类。public class MemberService { private static final MapString, Member MEMBER_STORE new HashMap(); // 注册新会员 public Member register(String phone, String nickname) { String memberId M System.currentTimeMillis(); Member member new Member(memberId, phone, nickname); MEMBER_STORE.put(memberId, member); return member; } // 积分变动 public void addPoints(String memberId, long delta) { Member member MEMBER_STORE.get(memberId); if (member null) { throw new IllegalArgumentException(会员不存在: memberId); } member.addPoints(delta); if (member.canUpgrade()) { member.upgrade(); } } }你注意我为什么把addPoints的校验逻辑放在服务类里同时Member类自己的addPoints也有校验。这就是“层层设防”Member类确保“任何一个Member对象都不可能被非法扣分”MemberService确保“业务操作前先找到正确的对象”。职责不同防御的层面也不同。以后你读Spring之类的框架代码会感觉到框架本身就是由一大堆类构成的按这种层次思维去读会清晰很多。关于类设计还有一个特别容易被忽略的原则类尽量小而专不要贪大求全。“上帝类”是所有维护者的噩梦——一个类里塞上百个字段五十多个方法谁也不敢动它因为动一处就可能牵一发而动全身。你宁可把一个大类拆成几个各司其职的小类通过组合把它们拼起来。组合优先于继承也是面试被问烂了的一句老话真实含义就是继承关系太僵硬父子绑死组合模式灵活想换就换。后文我会用一个实际案例展示这个道理。6. 实战中的常见错误与排查思路类相关的“锅”到底怎么甩做了这么多年Java开发我可以负责任地告诉你和类相关的报错至少有一半不是因为语法不会写而是因为对“类的生命周期”和“引用关系”理解不到位。我把最常见的几种坑拿出来聊每一个都附上排查路径你以后遇到类似问题可以按这个思路走。6.1 NoClassDefFoundError和ClassNotFoundException这哥俩是所有Java初学者迟早会遇到的一对双胞胎很多人以为它们是一个东西实际上完全不同。ClassNotFoundException是“找类没找到”发生在类加载阶段——类加载器去加载类的时候在类路径里没有找到你指定的类。NoClassDefFoundError是“类曾经出现过但现在不可用”发生在类加载成功之后、真正使用它的时候——更准确说是类加载过程中依赖它的另一个环节失败。排查思路很明确第一步确认类路径classpath里到底有没有这个类不管是jar包还是编译输出目录。第二步看有没有“依赖顺序”问题比如A类需要用到B类但B没被编译进去或者B的版本不对。第三步检查是否混淆了依赖范围——你的主程序用的是编译时的jar但运行时被另一个低版本jar覆盖了这种问题在Maven/Gradle多模块项目里经常出现。6.2 类初始化静态块里的顺序陷阱我之前说到静态代码块和静态变量的执行顺序是代码顺序优先这个陷阱在实际项目里会以更隐蔽的形式出现。比如你在静态代码块里调用一个静态方法而这个静态方法又依赖另一个静态字段一旦字段赋值顺序没安排好结果就是运行时直接NullPointerException或者值全是0。这类问题在面试现场很难编因为它是编译期完全合法的代码运行时才炸。我给你的排查建议是如果你在类里看到了静态代码块第一反应应该是“这里可能有顺序问题”然后检查它访问了哪些静态字段或静态方法确认它们的初始化顺序。如果静态代码块逻辑比较长最好把它拆成一个静态方法按明确顺序调用比堆在一个大块里清晰得多。6.3 引用传递导致的“对象污染”这是我个人见过最多、也最隐蔽的一种bug你以为调用了一个新对象实际上改的还是旧对象。看这段代码Member a new Member(M001, 13800000000, 小明, 1, 0L); Member b a; b.addPoints(500L); System.out.println(a.getPoints()); // 输出 500很多人看到Member b a;脑子里的画面是“把a复制一份给b”但实际效果是b和a指向同一个对象操作b等于操作a。这种问题在方法参数传对象时尤其阴险方法内部改了参数的字段调用方立刻感知到变化。你如果不想让外部改动你的对象要么在传参时用构造方法或工厂方法生成一个新对象要么把字段设为private并通过受控的方法去修改。还有一个相关的经典面试题Java有没有“真正的按引用传递”答案是没有Java只有按值传递。Member b a;里传的不是对象而是引用的副本——b和a拿着两份“标签”但标签指向同一个蛋糕。你把b指向另一个对象a完全不受影响但通过b去改对象的字段a看到的值当然就变了因为那个蛋糕本来就是同一个。6.4 用equals而不是比较对象基本类型用比较值引用类型用比较引用地址这个知识点很多人知道。但一写代码就把User对象拿出来比较的真的是成片成片地出现。两个User对象就算每个字段都相等只要不是同一个引用的结果就是false。正确的姿势是重写equals()方法同时记得重写hashCode()然后手写每个字段的比较逻辑。你可能会偷懒用IDE自动生成那没问题但建议你看一眼生成的代码理解为什么它比较的是每个关键字段而不是整个对象。这个理解有了以后用contains方法、HashMap的key、List的去重操作时心里就有底了。6.5 类和对象的典型调试思路我自己排查类相关问题时通常会按这套思路走先看异常类型编译期错误还是运行期错误ClassNotFoundException还是NullPointerException再看异常栈里的类名和方法名是哪个类触发了问题它是在加载阶段炸的还是在初始化阶段炸的还是在某个方法调用时炸的然后断点打在构造方法的第一行和关键字段赋值处观察对象产生过程中的每个字段值。最后检查使用方这个类被谁new出来的new完之后有没有被其他引用意外修改这套“从出生到使用”的排查路径虽然朴素但比瞎猜高效得多。尤其是那些只在一部分环境下出现的诡异问题基本都是类加载顺序或引用共享导致的沿着生命周期查一遍基本就跑不掉。7. 类设计中的几个实战心得工具、模板和习惯前面讲了那么多最后把我个人在工作里沉淀的一些类设计习惯分享给你。这些不算什么高深理论但确实帮我少踩了很多坑。第一个习惯优先考虑组合而不是继承。继承看起来很省事B类extends A类立刻拥有了A的所有能力。但代价是B类和A类永久绑定A一改动B可能直接就编译挂了。实际项目里我遇到过太多因为三层继承导致的行为混乱——子类既继承了父类的状态又重写了父类的方法还调用了父类的私有逻辑整个类的行为已经不是任何一个单一层级能解释清楚的了。而组合很单纯B里放一个A类型的字段需要的时候调用A的方法。想换实现把字段类型换成接口替换实现类就行。代码的可读性和可维护性都远胜于硬继承。第二个习惯写类之前先写注释把“这个类到底是干什么的”写清楚。我见过很多类代码本身并不复杂但因为你不知道这个类在业务里的位置看半天也不知道它存在的意义。类注释不需要长篇大论一两句话讲清楚就行“该类表示订单中的商品行包含商品快照信息和购买数量一个订单包含多个OrderItem。”这份注释就是给三个月后的你或者你同事看的。第三个习惯字段能不可变就不可变成员能不暴露就不暴露。Java的final和private看似简单但组合起来就是你能写出的最坚固的第一道防线。你永远不知道将来的哪段代码会不小心改掉一个不应该被修改的字段写类的时候多敲一个final以后可能就少排查一个通宵。第四个习惯把工具类用static方法做成无状态全局服务。世界上有些类天生不应该有对象状态比如时间格式化工具、字符串处理工具它们只是“方法集合”。你把它们做成普通类每次都new一个再调用纯属浪费内存且没有意义。直接public final class XxxUtils加私有构造方法加静态方法是Java社区非常成熟的惯例。注意类名上我特意加了final这样别人就没法继承你的工具类去“扩展”从根源上防止有人给工具类添加状态。第五个习惯构造方法保持精简复杂构建交给工厂方法或建造者。如果一个构造函数需要五六个甚至更多参数调用方很容易写错顺序而且很难看懂哪一个是哪一个。给你一个实际可行的方案用静态工厂方法代替构造方法。public class Member { private String memberId; private String phone; private String nickname; private Member() { // 私有构造不允许直接 new Member() } public static Member createNewByPhone(String phone) { Member m new Member(); m.phone phone; m.nickname 新会员; return m; } public static Member createVipWithFullInfo(String id, String phone, String nickname) { Member m new Member(); m.memberId id; m.phone phone; m.nickname nickname; return m; } }你可以看到构造方法被私有化了外部不能直接new Member()只能通过有语义的方法名去创建。这个模式的好处是调用方不需要知道内部如何组装看到的只是“创建一个手机号注册的新会员”或者“创建一个全信息VIP”。如果哪天创建逻辑变了你只需要去改工厂方法内部调用方一行都不用动。如果参数实在太多还有一个更进阶的方案——建造者模式Builder Pattern。它的核心思路是用一个内部静态类Builder一步步设置字段最后调build()生成对象。Java自带的StringBuilder就是这种思路的经典例子。不过日常业务开发里我觉得“静态工厂方法”已经能覆盖大部分场景了只有字段特别多而且每个字段都有默认值的DTO才值得上Builder。第六个习惯也是最后一条类的小众分类要和业务模型对齐不要为了分类而分类。很多人看了一些设计模式的书就开始天天想着抽象类、接口、工厂模式结果一个简单的订单模块被拆成八个类。其实分类的意义是让代码的维护成本降低而不是让代码看起来“高级”。如果你自己都说不清某个类在业务里的角色、为什么必须单独存在那它大概率就不该存在。把类和真实世界的模型对上号是你写代码时的锚。8. 一些适合练手的类级别小项目理论讲再多不动手都是白搭。我列几个我平时带新人时常让他们练的类设计小项目难度从低到高排你可以自己选择从哪个开始第一个手写一个商品库存系统。至少设计三个实体类商品、库存记录、供应商一个服务类库存管理服务一个入口类命令行交互。重点练字段约束、构造方法重载、类之间的组合关系。第二个一个简单的模拟银行账号系统。抽象类Account、子类储蓄账号、信用账号、投资账号接口如InterestCalculator。重点练抽象类和接口的使用场景、方法重写、向上转型。第三个仿写一个极简的通知框架。定义一个Notifier接口实现几个不同的通知类邮件、短信、站内信再用一个聚合类去管理多种通知。重点练接口解耦、策略模式的感觉。第四个设计一个带Builder模式的配置类。字段很多涵盖数据库配置、缓存配置、线程池配置用静态内部类Builder来构建。重点练Builder模式、字段默认值、空值校验。这几个项目做完你对“类的创建与分类”的理解绝对会上升到另一个层次——不再是背语法而是真的能根据业务场景设计出合适的类结构。等你做完回头来看这篇文章很多你之前划过的重点会变成你自己的直觉。
网站建设高端定制企业官网