阅读开源框架,总结Java类的定义

简介: 阅读开源框架,总结Java类的定义

Java的类是自定义的引用类型,是对职责相关的行为与数据的一种封装,用以表现一种业务领域或者技术领域的概念。在不同的场景,类包含的成员可能有所不同,大体可以分为如下五类:

  • 数据类:可以视为是持有数据的容器,类的成员只包含了字段,以及与字段有关的get/set方法
  • 实体类:既包含了体现状态的字段,又包含了操作这些状态的方法
  • 服务类:只有方法(行为)没有字段(状态),可以理解为提供内聚职责的服务
  • 函数类:如果定义的公开方法只有唯一一个,可以理解为它封装的其实是一个函数,通常用匿名类或者Lambda表示
  • 工具类:只包含一系列静态方法,通常不支持对该类型的实例化


数据类



在Presto框架中定义的ClientSession可以认为是这样一种数据类。除了构造函数外,它只定义了字段与对应的get()方法(实际上,在框架的源代码中,在ClientSession类中还定义了一系列静态工厂方法,但本质上说,ClientSession还是一个数据类),用以持有客户端Session所必须的数据:

public class ClientSession {
   private final URI server;
   private final String use;
   private final String source;
   private final String clientInfo;
   private final String catalog;
   private final String schema;
   private final TimeZoneKey timeZone;
   private final Locale locale;
   private final Map<String, String> properties;
   private final Map<String, String> preparedStatements;
   private final String transactionId;
   private final boolean debug;
   private final Duration clientRequestTimeout;
   public ClientSession(
           URI server,
           String user,
           String source,
           String clientInfo,
           String catalog,
           String schema,
           String timeZoneId,
           Locale locale,
           Map<String, String> properties,
           String transactionId,
           boolean debug,
           Duration clientRequestTimeout)    {
       this(server, user, source, clientInfo, catalog, schema, timeZoneId, locale, properties, emptyMap(), transactionId, debug, clientRequestTimeout);
   }
   public ClientSession(
           URI server,
           String user,
           String source,
           String clientInfo,
           String catalog,
           String schema,
           String timeZoneId,
           Locale locale,
           Map<String, String> properties,
           Map<String, String> preparedStatements,
           String transactionId,
           boolean debug,
           Duration clientRequestTimeout)   {
       this.server = requireNonNull(server, "server is null");
       this.user = user;
       this.source = source;
       this.clientInfo = clientInfo;
       this.catalog = catalog;
       this.schema = schema;
       this.locale = locale;
       this.timeZone = TimeZoneKey.getTimeZoneKey(timeZoneId);
       this.transactionId = transactionId;
       this.debug = debug;
       this.properties = ImmutableMap.copyOf(requireNonNull(properties, "properties is null"));
       this.preparedStatements = ImmutableMap.copyOf(requireNonNull(preparedStatements, "preparedStatements is null"));
       this.clientRequestTimeout = clientRequestTimeout;
       // verify the properties are valid
       CharsetEncoder charsetEncoder = US_ASCII.newEncoder();
       for (Entry<String, String> entry : properties.entrySet()) {
           checkArgument(!entry.getKey().isEmpty(), "Session property name is empty");
           checkArgument(entry.getKey().indexOf('=') < 0, "Session property name must not contain '=': %s", entry.getKey());
         checkArgument(charsetEncoder.canEncode(entry.getKey()), "Session property name is not US_ASCII: %s", entry.getKey());
           checkArgument(charsetEncoder.canEncode(entry.getValue()), "Session property value is not US_ASCII: %s", entry.getValue());
       }
   }
   public URI getServer()    {
       return server;
   }
   public String getUser()    {
       return user;
   }
   public String getSource()    {
       return source;
   }
   public String getClientInfo()    {
       return clientInfo;
   }
   public String getCatalog()    {
       return catalog;
   }
   public String getSchema()    {
       return schema;
   }
   public TimeZoneKey getTimeZone()    {
       return timeZone;
   }
   public Locale getLocale()    {
       return locale;
   }
   public Map<String, String> getProperties()    {
       return properties;
   }
   public Map<String, String> getPreparedStatements()    {
       return preparedStatements;
   }
   public String getTransactionId()    {
       return transactionId;
   }
   public boolean isDebug()    {
       return debug;
   }
   public Duration getClientRequestTimeout()    {
       return clientRequestTimeout;
   }
   @Override
   public String toString()    {
       return toStringHelper(this)
               .add("server", server)
               .add("user", user)
               .add("clientInfo", clientInfo)
               .add("catalog", catalog)
               .add("schema", schema)
               .add("timeZone", timeZone)
               .add("locale", locale)
               .add("properties", properties)
               .add("transactionId", transactionId)
               .add("debug", debug)
               .toString();
   }
}


这样包含数据或状态的对象通常会作为参数在方法调用之间传递,体现了诸如配置、视图模型、服务传输数据、协议数据等概念。除此之外,我们应尽量避免定义这样的对象去体现某种业务概念,因为基于“信息专家”模式,好的面向对象设计应该是将数据与操作这些数据的行为封装在一起。


实体类



这是最为常见的一种类定义,也是符合面向对象设计原则的,前提是定义的类必须是高内聚的,原则上应该满足单一职责原则。例如JDK定义的Vector展现了一种数据结构,因而它持有的字段与方法应该仅仅与队列操作与状态有关:

public class Vector<E>
   extends AbstractList<E>
   implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
   protected Object[] elementData;
   protected int elementCount;
   protected int capacityIncrement;
   public Vector(int initialCapacity, int capacityIncrement) {
       super();
       if (initialCapacity < 0)
           throw new IllegalArgumentException("Illegal Capacity: "+
                                             initialCapacity);
       this.elementData = new Object[initialCapacity];
       this.capacityIncrement = capacityIncrement;
   }
 public Vector(int initialCapacity) {
       this(initialCapacity, 0);
   }
 public synchronized void setSize(int newSize) {
       modCount++;
       if (newSize > elementCount) {
           ensureCapacityHelper(newSize);
       } else {
           for (int i = newSize ; i < elementCount ; i++) {
               elementData[i] = null;
           }
       }
       elementCount = newSize;
   }
   public synchronized int size() {
       return elementCount;
   }
   public synchronized boolean isEmpty() {
       return elementCount == 0;
   }
   public boolean contains(Object o) {
       return indexOf(o, 0) >= 0;
   }
   public synchronized E firstElement() {
       if (elementCount == 0) {
           throw new NoSuchElementException();
       }
       return elementData(0);
   }
   public synchronized void insertElementAt(E obj, int index) {
       modCount++;
       if (index > elementCount) {
           throw new ArrayIndexOutOfBoundsException(index
                                                   + " > " + elementCount);
       }
       ensureCapacityHelper(elementCount + 1);
       System.arraycopy(elementData, index, elementData, index + 1, elementCount - index);
       elementData[index] = obj;
       elementCount++;
   }
   public synchronized void addElement(E obj) {
       modCount++;
       ensureCapacityHelper(elementCount + 1);
       elementData[elementCount++] = obj;
   }
   public synchronized boolean removeElement(Object obj) {
       modCount++;
       int i = indexOf(obj);
       if (i >= 0) {
           removeElementAt(i);
           return true;
       }
       return false;
   }
   public synchronized void removeAllElements() {
       modCount++;
       // Let gc do its work
       for (int i = 0; i < elementCount; i++)
           elementData[i] = null;
       elementCount = 0;
   }
}

如下类的定义则体现了一种业务概念,方法changePriceTo()实际上表现的是一种业务规则,而它要操作的数据就是Product类自身持有的字段sellingPrice:

public class Product extends Entity<Identity> {
   private final List<Option> options;
   private Price sellingPrice;
   private Price retailPrice;
   public Product(Identity id, Price sellingPrice, Price retailPrice)  {
       super(id);
       this.sellingPrice = sellingPrice;
       if (!sellingPriceMatches(retailPrice) {
           throw new PricesNotInTheSameCurrencyException("Selling and retail price must be in the same currency");
       }
       this.retailPrice = retailPrice;
       options = new List<Option>();
   }
   public void changePriceTo(Price newPrice) {
       if (!sellingPriceMatches(newPrice)) {
           throw new PricesNotInTheSameCurrencyException("You cannot change the price of this product to a different currency");
       }
       sellingPrice = newPrice;
   }
   public Price savings() {
       Price savings = retailPrice.minus(sellingPrice);
       if (savings.isGreaterThanZero())
           return savings;
       else
           return new Price(0m, sellingPrice.currency);
   }
   private bool sellingPriceMatches(Price retailPrice) {
       return sellingPrice.sameCurrency(retailPrice);
   }
   public void add(Option option) {
       if (!this.contains(option))
           options.Add(option);
       else
           throw new ProductOptionAddedNotUniqueException(string.Format("This product already has the option {0}", option.ToString()));
   }
   public bool contains(Option option) {
       return options.Contains(option);
   }
}


服务类



只有方法没有状态的类定义是对行为的封装,行为的实现要么是通过操作内部封装的不可变私有数据,要么是通过操作传入的参数对象实现对状态的修改。由于参数传入的状态与服务类自身没有任何关系,因此这样的类通常也被视为无状态的类。以下代码是针对升级激活包的验证服务:

public class PreActivePackageValidator {
   public long validatePreActivePackage(ActiveManifest  activeManifest) {
         validateSamePackageType(activeManifest);
         validateNoTempPackage(activeManifest);
         validateNoPackageRunning(activeManifest);
         validateAllPackagesBeenDownloaded(activeManifest);
         validateNoFatherPackageBakStatus(activeManifest);
         validatePackageNum(activeManifest);
   }
   private void validateSamePackageType(ActiveManifest  activeManifest) {
       int packakeType = activeManifest.getPackageType();
       for (UpagrdePackage pkg : activeManifest.getPackages()) {
           if (packageType != pkg.getPackageType()) {
               throw new PackagePreActiveException("pre active exist different type package");
           }
       }
   }
}


服务类还可以操作外部资源,例如读取文件、访问数据库、与第三方服务通信等。例如airlift框架定义的ConfigurationLoader类,就提供加载配置文件内容的服务:

public class ConfigurationLoader {
   public Map<String, String> loadProperties()
           throws IOException    {
       Map<String, String> result = new TreeMap<>();
       String configFile = System.getProperty("config");
       if (configFile != null) {
           result.putAll(loadPropertiesFrom(configFile));
       }
       result.putAll(getSystemProperties());
       return ImmutableSortedMap.copyOf(result);
   }
   public Map<String, String> loadPropertiesFrom(String path)
           throws IOException    {
       Properties properties = new Properties();
       try (Reader reader = new FileReader(new File(path))) {
           properties.load(reader);
       }
       return fromProperties(properties);
   }
   public Map<String, String> getSystemProperties()    {
       return fromProperties(System.getProperties());
   }
}


函数类



可以将函数类理解为设计一个类,它仅仅实现了一个接口,且该接口只定义一个方法。使用时,我们会基于依赖倒置原则(DIP)从接口的角度使用这个类。为了重用的目的,这个类可以单独被定义,也可能体现为匿名类,或者Java 8中的Lambda表达式。


单独类形式


例如,在Presto中定义了PagesIndexComparator接口,提供了比较方法以用于支持对页面索引的排序。接口的定义为:

public interface PagesIndexComparator {
   int compareTo(PagesIndex pagesIndex, int leftPosition, int rightPosition);
}


Presto定义了该接口的实现类SimplePagesIndexComparator,该类就是一个函数类:

public class SimplePagesIndexComparator
       implements PagesIndexComparator {
   private final List<Integer> sortChannels;
   private final List<SortOrder> sortOrders;
   private final List<Type> sortTypes;
   public SimplePagesIndexComparator(List<Type> sortTypes, List<Integer> sortChannels, List<SortOrder> sortOrders)   {
       this.sortTypes = ImmutableList.copyOf(requireNonNull(sortTypes, "sortTypes is null"));
       this.sortChannels = ImmutableList.copyOf(requireNonNull(sortChannels, "sortChannels is null"));
       this.sortOrders = ImmutableList.copyOf(requireNonNull(sortOrders, "sortOrders is null"));
   }
   @Override
   public int compareTo(PagesIndex pagesIndex, int leftPosition, int rightPosition)   {
       long leftPageAddress = pagesIndex.getValueAddresses().getLong(leftPosition);
       int leftBlockIndex = decodeSliceIndex(leftPageAddress);
       int leftBlockPosition = decodePosition(leftPageAddress);
       long rightPageAddress = pagesIndex.getValueAddresses().getLong(rightPosition);
       int rightBlockIndex = decodeSliceIndex(rightPageAddress);
       int rightBlockPosition = decodePosition(rightPageAddress);
       for (int i = 0; i < sortChannels.size(); i++) {
           int sortChannel = sortChannels.get(i);
           Block leftBlock = pagesIndex.getChannel(sortChannel).get(leftBlockIndex);
           Block rightBlock = pagesIndex.getChannel(sortChannel).get(rightBlockIndex);
           SortOrder sortOrder = sortOrders.get(i);
           int compare = sortOrder.compareBlockValue(sortTypes.get(i), leftBlock, leftBlockPosition, rightBlock, rightBlockPosition);
           if (compare != 0) {
               return compare;
           }
       }
       return 0;
   }
}


我们看到SimplePagesIndexComparator类的逻辑相对比较复杂,构造函数也需要传入三个参数:List<Type> sortTypes,List<Integer> sortChannels和List<SortOrder> sortOrders。虽然从接口的角度看,其实代表的是compare的语义,但由于逻辑复杂,而且需要传入三个对象帮助对PagesIndex进行比较,因而不可能实现为匿名类或者Lambda表达式。在Presto中,对它的使用为:

public class PagesIndexOrdering {
   private final PagesIndexComparator comparator;
   public PagesIndexOrdering(PagesIndexComparator comparator)  {
       this.comparator = requireNonNull(comparator, "comparator is null");
   }
   public PagesIndexComparator getComparator()  {
       return comparator;
   }
   /**
   * Returns the index of the median of the three positions.
   */
   private int median3(PagesIndex pagesIndex, int a, int b, int c)    {
       int ab = comparator.compareTo(pagesIndex, a, b);
       int ac = comparator.compareTo(pagesIndex, a, c);
       int bc = comparator.compareTo(pagesIndex, b, c);
       return (ab < 0 ?
               (bc < 0 ? b : ac < 0 ? c : a) :
               (bc > 0 ? b : ac > 0 ? c : a));
   }
}


匿名类形式


同样在该框架下定义的IntComparator接口,它的实现就完全不同了。首先是该接口的定义:

public interface IntComparator {
   /** Compares the given primitive types.
   *
   * @see java.util.Comparator
   * @return A positive integer, zero, or a negative integer if the first
   * argument is greater than, equal to, or smaller than, respectively, the
   * second one.
   */
   int compare(int k1, int k2);
}
在针对整型数据提供排序功能时,用到了IntComparator接口:
public final class IntBigArray {
   public void sort(int from, int to, IntComparator comparator)    {
       IntBigArrays.quickSort(array, from, to, comparator);
   }
}


但由于提供整型数据的比较逻辑相对简单,在Presto中并没有定义显式的函数类,而是使用了Lambda表达式:

groupIds.sort(0, groupByHash.getGroupCount(), (leftGroupId, rightGroupId) ->
               Long.compare(groupByHash.getRawHash(leftGroupId), groupByHash.getRawHash(rightGroupId)));

这里的Lambda表达式其实也可以理解为是一个函数类。


函数重用形式


还有一种特殊的函数类,它的定义形式与后面介绍的工具类非常相似,同样是定义了一组静态方法,但它的目的不是提供工具或辅助功能,而是将其视为函数成为被重用的单元。这时,需要用到Java 8提供的方法引用(method reference)语法。例如我们要对List<Apple>集合进行过滤,过滤条件分别为颜色与重量,这时可以在Apple类中定义两个静态方法:

public class Apple {
   public static boolean isGreenApple(Apple apple) {
       return "green".equals(apple.getColor());
   }
   public static boolean isHeavyApple(Apple apple) {
       return apple.getWeight() > 150;
   }
}


这两个方法实际上满足函数接口Predicate<Apple>的定义,因此可以在filter方法中传入这两个方法的引用:

public List<Apple> filter(Predicate<Apple> predicate) {
   ArrayList<Apple> result = new ArrayList<>();
   for (Apple apple : apples) {
       if (predicate.test(apple)) {
           result.add(apple);
       }
   }
   return result;
}
public List<Apple> filterGreenApples() {
   return filter(Apple::isGreenApple);
}
public List<Apple> filterHeavyApples() {
   return filter(Apple::isHeavyApple);
}


此时Apple类可以认为是一个函数类,但准确地说法是一系列可以被重用的函数的容器。与工具类不同的是,这些函数并不是被直接调用,本质上讲,其实是作为“高阶函数”被传递给其他方法而被重用。虽然说实例方法也可以采用这种方式而被重用,但静态方法的调用会更加简单。


工具类


在许多项目或开源项目中,随处可见工具类的身影。无需实例化的特性使得我们使用工具类的方式时变得非常的便利,也不需要考虑状态的维护。然而越是方便,我们越是要警惕工具类的陷阱——设计出臃肿庞大无所不能的上帝工具类。工具类仍然要遵循高内聚的原则,只有强相关的职责才能放到同一个工具类中。


在定义工具类时,通常有三类命名范式:

  • 名词复数形式:工具类其实就是一系列工具方法的容器,当我们要针对某种类型(或对象)提供工具方法时,可以直接将工具类命名为该类型的复数形式,例如操作Collection的工具类可以命名为Collections,操作Object的工具类可以命名为Objects,而与前置条件有关的工具类则被命名为Preconditions。
  • 以Util为后缀:这体现了工具(Utility)的语义,当我们在类名中看到Util后缀时,就可以直观地了解到这是一个工具类。例如ArrayUtil类是针对数组的工具类,DatabaseUtil是针对数据库操作的工具类,UuidUtil是针对Uuid的工具类。
  • 以Helper为后缀:这种命名相对较少,但许多框架也采用这种命名方式来体现“辅助类”的含义。例如在Druid框架中,就定义了JobHelper、GroupByQueryHelper等辅助类。


工具类是无需实例化的,因此在定义工具类时,尽可能将其声明为final类,并为其定义私有的构造函数。例如Guava框架提供的Preconditions工具类:

public final class Preconditions {
   private Preconditions() {
   }
   public static void checkArgument(boolean expression) {
       if(!expression) {
           throw new IllegalArgumentException();
       }
   }
   //other util methods
}
相关文章
|
1月前
|
存储 Java
【源码】【Java并发】【ThreadLocal】适合中学者体质的ThreadLocal源码阅读
前言 下面,跟上主播的节奏,马上开始ThreadLocal源码的阅读( ̄▽ ̄)" 内部结构 如下图所示,我们可以知道,每个线程,都有自己的threadLocals字段,指向ThreadLocalMap
393 81
【源码】【Java并发】【ThreadLocal】适合中学者体质的ThreadLocal源码阅读
|
2月前
|
Java 开发者
重学Java基础篇—Java类加载顺序深度解析
本文全面解析Java类的生命周期与加载顺序,涵盖从加载到卸载的七个阶段,并深入探讨初始化阶段的执行规则。通过单类、继承体系的实例分析,明确静态与实例初始化的顺序。同时,列举六种触发初始化的场景及特殊场景处理(如接口初始化)。提供类加载完整流程图与记忆口诀,助于理解复杂初始化逻辑。此外,针对空指针异常等问题提出排查方案,并给出最佳实践建议,帮助开发者优化程序设计、定位BUG及理解框架机制。最后扩展讲解类加载器层次与双亲委派机制,为深入研究奠定基础。
88 0
|
1月前
|
前端开发 Java 物联网
智慧班牌源码,采用Java + Spring Boot后端框架,搭配Vue2前端技术,支持SaaS云部署
智慧班牌系统是一款基于信息化与物联网技术的校园管理工具,集成电子屏显示、人脸识别及数据交互功能,实现班级信息展示、智能考勤与家校互通。系统采用Java + Spring Boot后端框架,搭配Vue2前端技术,支持SaaS云部署与私有化定制。核心功能涵盖信息发布、考勤管理、教务处理及数据分析,助力校园文化建设与教学优化。其综合性和可扩展性有效打破数据孤岛,提升交互体验并降低管理成本,适用于日常教学、考试管理和应急场景,为智慧校园建设提供全面解决方案。
224 70
|
1月前
|
Java
【源码】【Java并发】【ReentrantLock】适合中学者体质的ReentrantLock源码阅读
因为本文说的是ReentrantLock源码,因此会默认,大家对AQS有基本的了解(比如同步队列、条件队列大概> 长啥样?)。 不懂AQS的小朋友们,你们好呀!也欢迎先看看这篇
79 13
【源码】【Java并发】【ReentrantLock】适合中学者体质的ReentrantLock源码阅读
|
1月前
|
Java 数据安全/隐私保护
Java 类和对象
本文介绍了Java编程中类和对象的基础知识,作为面向对象编程(OOP)的核心概念。类是对象的蓝图,定义实体类型;对象是具体实例,包含状态和行为。通过示例展示了如何创建表示汽车的类及其实例,并说明了构造函数、字段和方法的作用。同时,文章还探讨了访问修饰符的使用,强调封装的重要性,如通过getter和setter控制字段访问。最后总结了类与对象的关系及其在Java中的应用,并建议进一步学习继承等概念。
|
2月前
|
存储 JSON Java
《从头开始学java,一天一个知识点》之:方法定义与参数传递机制
**你是否也经历过这些崩溃瞬间?** - 看了三天教程,连`i++`和`++i`的区别都说不清 - 面试时被追问&quot;`a==b`和`equals()`的区别&quot;,大脑突然空白 - 写出的代码总是莫名报NPE,却不知道问题出在哪个运算符 🚀 这个系列就是为你打造的Java「速效救心丸」!我们承诺:每天1分钟,地铁通勤、午休间隙即可完成学习;直击痛点,只讲高频考点和实际开发中的「坑位」;拒绝臃肿,没有冗长概念堆砌,每篇都有可运行的代码标本。上篇:《输入与输出:Scanner与System类》 | 下篇剧透:《方法重载与可变参数》。
63 25
|
2月前
|
存储 监控 Java
《从头开始学java,一天一个知识点》之:数组入门:一维数组的定义与遍历
**你是否也经历过这些崩溃瞬间?** - 看了三天教程,连`i++`和`++i`的区别都说不清 - 面试时被追问&quot;`a==b`和`equals()`的区别&quot;,大脑突然空白 - 写出的代码总是莫名报NPE,却不知道问题出在哪个运算符 这个系列就是为你打造的Java「速效救心丸」!我们承诺:每天1分钟,地铁通勤、午休间隙即可完成学习;直击痛点,只讲高频考点和实际开发中的「坑位」;拒绝臃肿,没有冗长概念堆砌,每篇都有可运行的代码标本。明日预告:《多维数组与常见操作》。 通过实例讲解数组的核心认知、趣味场景应用、企业级开发规范及优化技巧,帮助你快速掌握Java数组的精髓。
77 23
|
2月前
|
缓存 安全 Java
《从头开始学java,一天一个知识点》之:输入与输出:Scanner与System类
你是否也经历过这些崩溃瞬间?三天教程连`i++`和`++i`都说不清,面试时`a==b`与`equals()`区别大脑空白,代码总是莫名报NPE。这个系列就是为你打造的Java「速效救心丸」!每天1分钟,地铁通勤、午休间隙即可学习。直击高频考点和实际开发中的“坑位”,拒绝冗长概念,每篇都有可运行代码示例。涵盖输入输出基础、猜数字游戏、企业编码规范、性能优化技巧、隐藏技能等。助你快速掌握Java核心知识,提升编程能力。点赞、收藏、转发,助力更多小伙伴一起成长!
55 19
|
2月前
|
存储 监控 安全
重学Java基础篇—类的生命周期深度解析
本文全面解析了Java类的生命周期,涵盖加载、验证、准备、解析、初始化、使用及卸载七个关键阶段。通过分阶段执行机制详解(如加载阶段的触发条件与技术实现),结合方法调用机制、内存回收保护等使用阶段特性,以及卸载条件和特殊场景处理,帮助开发者深入理解JVM运作原理。同时,文章探讨了性能优化建议、典型异常处理及新一代JVM特性(如元空间与模块化系统)。总结中强调安全优先、延迟加载与动态扩展的设计思想,并提供开发建议与进阶方向,助力解决性能调优、内存泄漏排查及框架设计等问题。
73 5
|
2月前
|
机器学习/深度学习 人工智能 Java
Java机器学习实战:基于DJL框架的手写数字识别全解析
在人工智能蓬勃发展的今天,Python凭借丰富的生态库(如TensorFlow、PyTorch)成为AI开发的首选语言。但Java作为企业级应用的基石,其在生产环境部署、性能优化和工程化方面的优势不容忽视。DJL(Deep Java Library)的出现完美填补了Java在深度学习领域的空白,它提供了一套统一的API,允许开发者无缝对接主流深度学习框架,将AI模型高效部署到Java生态中。本文将通过手写数字识别的完整流程,深入解析DJL框架的核心机制与应用实践。
114 3