软件开发中的 8 个伦理问题示例

软件开发中的 8 个伦理问题示例

随着软件在人类生活的方方面面根深蒂固,开发者对其客户负有伦理责任。我来来探讨如何承担这一责任。


传统上,伦理实践并非软件开发的一部分。软件并非总是对日常生活有直接影响,且开发速度缓慢。
在现代社会中,人们在生活的各个方面都会遇到软件。人工智能 (AI)、大数据和数据分析 (data analytics) 都会对个人产生切实的影响。
尽管软件开发人员主要在企业的幕后工作,但他们在项目过程中的决定,在合规性 (compliance)、公平性 (fairness)、诚信 (integrity) 和信任 (trust) 方面,可能对世界产生超乎寻常的影响——无论是好是坏。行业中的每个人都应该意识到软件开发中的社会与伦理问题。

以下是一些伦理问题的示例以及开发者可以如何解决它们:

  • 成瘾性设计 (Addictive design)。
  • 企业拥有个人数据 (Corporate ownership of personal data)。
  • 算法偏见 (Algorithmic bias)。
  • 薄弱的网络安全和个人身份信息 (PII) 保护 (Weak cybersecurity and personally identifiable information (PII) protection)。
  • 过度强调功能 (Overemphasis on features)。
  • 缺乏透明度 (Lack of transparency)。
  • 环境影响 (Environmental impact)。
  • 人权影响 (Human rights impact)。

1. 成瘾性设计 (Addictive design)

每位开发者都渴望创建人们喜欢使用的程序——这只是良好的用户体验 (UX) 设计。问题在于,有些团队设计的应用程序让人们爱不释手。这引发了关于社交媒体等数字平台角色的伦理担忧。
人道技术中心 (Center for Humane Technology) 的 Tristan Harris 等批评者认为,社交媒体公司从愤怒、困惑、成瘾和抑郁中获利——从而将我们的福祉和民主置于风险之中。值得注意的是,Harris 在谷歌工作时,因其关于推动成瘾性技术设计以及公司在社会中的道德责任的演讲而走红。
在消费者喜爱的产品和劫持他们注意力的产品之间取得伦理平衡,更像是一门艺术而非科学。在产品创建和更新中,请问以下问题:

  • 谁受益?
  • 他们如何受益?
  • 他们在多大程度上受益?
  • 是否有保障用户健康和理智的措施?
  • 包括通过 AI 和机器学习 (machine learning) 进行的货币化 (monetization) 以及客户数据收集和使用有多公开?这些实践有多透明?

技术诚信委员会 (Technology Integrity Council) 创始执行董事 David K. Bain 将 Duolingo 和 TikTok 作为应用程序设计的两个对比示例。这两个应用程序都为它们的创造者带来了增长和收入,但它们对用户的益处性质不同。
Duolingo 的客户获得语言技能,并通过促进神经元生长 (neuronal growth) 和大脑可塑性 (brain plasticity) 的活动受到挑战。TikTok 用户获得文化知识,并通过让大脑沐浴在令人陶醉的神经递质 (neurotransmitters) 中的视频内容获得即时满足感。“基于此,许多成年人会说 Duolingo 的真正用户收益大于 TikTok,”Bain 说,但他补充道,他十几岁的女儿会不同意。
这两个应用程序对旨在防范成瘾性依赖的使用限制持有不同态度。Duolingo 鼓励一致性,并有力论证其使用与优化的学习曲线 (learning curves) 相关。Duolingo 肯定会揪着用户的衣领 (grabs users by the lapels) 要求他们完成每日配额 (daily quota) 并保持连续表现 (performance streaks)。但一旦每日活动完成,Duolingo 就会释放用户。相比之下,TikTok 通过本质上无限量的可消费媒体 (consumable media) 自助餐来吸引用户留下。
应用程序通常包含用户操纵 (user manipulation)、货币化方法 (monetization methods)、用于企业用途的用户数据收集 (user data collection for corporate use) 以及用于增强应用程序的机器学习算法 (machine learning algorithms)。透明的应用程序提供者会让用户对这些实践有一定程度的了解和理解。
以下是这一伦理方面在这两个示例应用程序中的体现:“Duolingo 的用户显然是强制每日计划的自愿受害者,但几乎可以肯定没有意识到广告和使用数据连接到一个更大的广告生态系统,”Bain 说。“TikTok 的用户,尤其是年轻用户,我非常肯定在很大程度上是快乐地没有意识到他们成瘾的方法和后果。”

2. 存疑的个人数据所有权 (Questionable personal data ownership)

随着设备和软件的发展,基于 AI 的生物识别 (biometric) 和其他关于客户的上下文数据 (contextual data) 处理有所增加。软件可以以令人恐惧的详细程度对用户进行画像 (profile users) 并预测行为。
“通常,伦理问题是关于如何处理这些数据,”广告验证和欺诈预防平台 TrafficGuard 的首席产品官 Miguel Lopes 说。这种伦理问题对于各种业务的开发者来说都是一个困境——不仅仅是那些登上新闻的社交媒体巨头。
算法 (algorithm) 指导信息收集和画像构建 (profile building),但随后的行动是故意的。开发者通常清楚这些数据在特定情境下的力量。
开发者可以帮助公司内其他角色理解技术选择对伦理考量的影响。
Lopes 说,伦理担忧的根本原因之一与企业如何产生收入以及如何激励开发者和业务经理有关。在许多情况下,公司将用户数据视为一种有价值的货币,并希望将其存储的数据货币化。“这些因素可能导致这些组织不道德地共享其用户数据,”他说。
开发者在个人数据和软件设计方面面临艰难抉择。他们可以在理解责任在于组织的前提下创建利用用户数据的系统,或者他们可以提出担忧,但面临因违背项目目标而可能受到惩罚的风险。现代技术公司的工作文化应让开发者能够毫无畏惧地提出个人数据所有权的担忧。
这类担忧在 Lopes 工作过的不同组织中激起了丰富的讨论,这些组织决定不提供免费服务层级。“我们分析了其中的含义,更倾向于通过销售我们的服务而不是用户数据来维持运营,并且不让我们的开发团队面临这些艰难的选择,”Lopes 说。公司内部的透明度是一个关键因素。开发者应该了解他们正在参与的项目的整个背景,而不仅仅是他们需要完成的模块。
公司应该让开发者能够轻松地提出担忧。人力资源 (HR) 部门可以创建机制,让开发者能够表达他们的担忧而无需担心报复,例如用于伦理问题的匿名热线。然后,组织应跟进并独立识别该用例是否违反了隐私、法律或伦理政策。

3. 算法偏见 (Algorithmic bias)

技术会放大现有的偏见。“当今开发者面临的一个更紧迫的伦理问题是偏见 (bias),”业务自动化平台 Pegasystems 的首席客户主管 Spencer Lentz 说。
偏见常常在未被察觉的情况下进入系统——Lentz 将偏见比作病毒。计算机本身没有内在的道德框架。软件只能反映其创造者的偏见。因此,开发者和数据科学家 (data scientists) 必须从训练数据 (training data) 和他们构建的算法中清除偏见。Lentz 说,从开发者的角度来看,偏见通常集中在出于错误的原因消除选项上。
当在不完整和有偏见的数据上训练时,AI 可能会产生有偏见的结果
当在不完整和有偏见的数据上训练时,AI 可能会产生有偏见的结果
近年的报道和研究说明了软件系统中的偏见如何能对特定人群延续系统性种族主义 (systemic racism),这会造成机会丧失、恶化医疗护理并增加监禁率。例如,在《Race After Technology》一书中,Ruha Benjamin 对一个案例提出了担忧:开发者未能将黑人的声音纳入训练 AI 语音识别算法中,理由是认为使用该应用程序的黑人较少。
高管、数据科学家和开发者必须创建一种组织文化,制定伦理准则,并赋予业务任何层级的个人在看到有问题时发声的权力。
“时至今日,模型中的偏见已众所周知,以至于 LLM 幻觉 (LLM hallucination) 已成为一个主流概念,”数据科学平台 Anaconda 的首席 AI 和创新官兼联合创始人 Peter Wang 说。“如今最大的风险是,人们被炒作和害怕落后的恐惧冲昏了头脑,以至于没有花时间勤奋地构建评估机制和实施治理 (governance)。作为一个行业,我们需要更加透明地说明企业 AI 项目的失败率有多高,这样管理者和高管才不会感到有必要在一致性 (alignment)、准确性 (accuracy) 和安全性 (safety) 等极其重要的话题上仓促行事。”
Wang 主张,是时候为 AI 提供商创建一个管理机构了,类似于美国医学会 (American Medical Association) 之于医生。该机构可以制定全行业的伦理准则和最佳实践 (best practices)。“这些技术在商业环境中仍然相对较新,我们都将从源于我们集体智慧和投入的伦理标准中受益,而不是让每个个体或组织自行决定,”他说。

4. 薄弱的安全性和 PII 保护 (Weak security and PII protection)

随着软件在我们的线上和线下环境中扮演越来越重要的角色,应用程序安全 (Application security) 的重要性日益增长。
开发者可能只在代码发布后才处理安全性问题,而不是在开发过程中。因此,软件社区缺乏安全的开发标准 (secure development standards)。
“重点几乎完全放在将产品推向市场上,”软件开发咨询公司 Bit Developers 的创始人兼首席软件架构师 Randolph Morris 说。一旦软件产品公开发布,重点就会转移到新功能和性能优化上,因此安全性仍然很少受到重视。
黑客 (Hackers) 和其他恶意行为者 (malicious actors) 会对真实的人造成切实的损害。一种被动应对 (reactionary approach) 应用程序安全的方法——在发现漏洞 (vulnerabilities) 时才进行修补——既不实用也不可行。
为了履行对客户安全的这一伦理责任,开发者需要接受教育,但通常只有专门的网络安全 (cybersecurity) 课程涉及这些主题。首先,让你的团队了解网络安全故障,例如 2015 年具有里程碑意义的 Anthem 医疗数据泄露事件 (Anthem medical data breach),其中 PII 以纯文本 (plain text) 形式存储在数据库中。“如果这些信息被加密 (encrypted),就不会那么容易使用和有价值地分发,”Morris 说。
此外,行业需要修订安全标准 (security standards)。组织可以采取更多措施来采用旨在保护 PII 的标准。支付卡行业数据安全标准 (Payment Card Industry Data Security Standard, PCI DSS) 和针对医疗保健应用程序的 HIPAA 是一个良好的开端,但开发者还应考虑其他形式的 PII——以及保护它的软件设计。
探索企业在应用程序设计中应负责任处理的不同类型的个人信息
探索企业在应用程序设计中应负责任处理的不同类型的个人信息

5. 优先考虑功能而非影响 (Prioritizing features over impact)**

许多伦理问题的核心在于一个决定:软件发布中的功能 (capabilities) 比它们可能产生的影响更重要。但仅仅因为你能够做某事,并不意味着你应该做。
“如果开发团队是根据其功能开发速度来衡量的,那么在设计或实施阶段,特定实施的伦理问题很可能不会被优先考虑,”应用安全平台 Black Duck 的软件供应链风险策略负责人 Tim Mackey 说。
企业本身必须为其软件中的伦理标准定下基调。以下是企业可以实现这一目标的一些方法:

  • 在整个软件生命周期(从设计到运营)中体现伦理优先事项。
  • 就开源软件许可和使用等伦理选择对员工进行培训。
  • 教导开发者、架构师、测试人员和其他软件团队成员符合法规和客户期望的数据管理实践 (data management practices)。
    Mackey 指出,开发者并不总是关注客户使用其软件的司法管辖区的最新立法行动新闻,但企业必须确保他们知情。
    工程领导层 (engineering leadership) 与法律团队之间的协作有助于避免伦理缺陷。例如,企业应关注客户的个人数据访问 (personal data access) 和保留 (retention)。数据访问控制 (Data access controls) 和日志记录机制 (logging mechanisms) 是在软件实施时启用的。负责创建功能性、用户友好型产品的开发者可能认为数据访问限制是另一个团队的责任。相反,应确保数据保护 (data protection) 是软件设计中包含的一项功能,从根本上防止未经授权的访问 (unauthorized access)。

6. AI 透明度的幻象 (Mirage of AI transparency)**

大型语言模型 (Large language models, LLMs) 在软件开发中扮演着越来越重要的角色,涉及生成代码和支持非结构化数据处理 (unstructured data processing) 等任务。由于 LLM 的复杂性,很容易忽视这些系统是如何被训练、配置和部署的——以及这对用户意味着什么。
“软件公司应该始终披露他们如何训练其 AI 引擎,”Lopes 说。“用户数据被收集的方式——通常是静默地收集并输入 LLM——引发了关于同意 (consent)、安全和自动化伦理界限的严重问题。”
已经出现了一些引人注目的案例,其中用户在平台上的互动被用于在没有任何通知的情况下静默训练 AI。“我们看到公司在未经同意的情况下收集行为数据 (behavioral data),本质上将用户变成了无偿贡献者,而这些模型有一天可能会取代他们的工作,”他继续说道。
一个训练有素的 AI 代理 (AI agent) 需要深度配置 (deep configuration)、监督 (supervision) 和昂贵的人力人才。“你认为通过跳过适当开发而节省的成本,几乎总是被一个专业化程度低的代理所造成的损害所掩盖——无论是安全风险、错误信息 (misinformation) 还是客户信任的丧失,”Lopes 说。
AI 伦理框架 (AI ethics frameworks) 旨在缓解上述部分问题
AI 伦理框架 (AI ethics frameworks) 旨在缓解上述部分问题

7. 环境影响 (Environmental impact)**

随着对气候变化影响(包括气温上升、洪水、火灾和其他不利天气条件)认识的提高,对各种活动环境影响的担忧日益增长。技术公司的活动也可能减少清洁水的获取、污染空气并减少生物多样性。
AI 使用量的增长带来了显著增加能源消耗以及随之而来的碳排放 (carbon emissions) 的风险。它还可能增加用于冷却数据中心的水系统压力,从而损害当地社区。云提供商也开始探索碳中和能源 (carbon-neutral energy sources),如核裂变电厂 (nuclear fission plants),同时掩盖了处理乏放射性燃料 (spent radioactive fuel) 相关的仍未解决的环境成本。
这些都是通常超出软件开发周期的大局考量,但在决定扩展由新 LLM 驱动的应用程序的潜在影响时,值得考虑。其他方面包括新软件应用程序可能鼓励不良环境选择的潜力。一个快时尚应用程序 (fast-fashion app) 可能会以产生更多浪费为代价来推动收入。

8. 社会与人权影响 (Social and human rights impact)**

考虑软件开发实践对人权影响的多个维度包括其对劳动力和社区的潜在影响。
在劳动力方面,一个担忧是所谓的“数据标注血汗工厂” (data labeling sweatshops) 的增长,其中涉及让工人接触有毒内容 (toxic content) 以改进 AI 系统中的内容审核 (content moderation)。尽管大多数企业并未直接参与此过程,但他们可能忽视了其 AI 和数据系统供应商和承包商所采用的做法。
此外,必须考虑优化应用程序在相对容易量化的方面(如仓库吞吐量 (warehouse throughput))的潜在影响,与在更难以量化的方面(如工人健康或心理健康)的潜在影响。风险在于,某些类型的生产力优化 (productivity optimizations) 可能对工人的生活及其对家庭和社区的贡献产生不利影响。
AI 系统在软件开发中的兴起推动了数据标注行业的增长,但通常缺乏监督。新的应用程序也有可能破坏社区的社会结构 (social fabric)。

合乎伦理的软件开发最佳实践 (Best practices for ethical software development)

以下是培养具有积极社会影响的实践的几种方法:

  • 主动性 (Proactivity):对软件工程选择对合乎伦理的软件开发乃至整个世界的影响和背景保持好奇。
  • 诚实 (Honesty):考虑软件工程选择如何可能与伦理原则相冲突,即使这对你个人或公司来说是不舒服的。
  • 问责制 (Accountability):确定在公司内部衡量和沟通伦理问题的方法,以确保每个人都达成共识。
  • 平衡社会责任与技术能力 (Balance social responsibility with technical ability):记住开发者可以帮助公司内其他角色理解技术选择对伦理考量的影响。

【注】本文译自:8 examples of ethical issues in software development

Spring框架中的Component与Bean注解

Spring Boot 中的 @Bean 与 @Component

Spring 的 @Component@Bean 注解的关键区别在于:@Bean 注解可用于暴露您自己编写的 JavaBeans,而 @Component 注解可用于暴露源代码由他人维护的 JavaBeans。
Spring 框架的核心是其控制反转 (IoC) 容器,它管理着应用程序中最重要的 JavaBeans 的生命周期。然而,IoC 容器并不管理应用程序可能需要的每一个 JavaBean。它只管理您明确要求它管理的 JavaBeans 的生命周期。
何时使用 Spring 的 @Bean 注解?
如果您自己编写了一个 JavaBean,可以直接在源代码中添加 Spring 的 @Bean 注解。这里我们要求 Spring 的 IoC 容器管理 Score 类所有实例的生命周期。

@Bean
public class Score {
    int wins, losses, ties;
}

何时使用 Spring 的 @Component 注解?
但是,如果您想让 Spring 的 IoC 容器管理来自 Jackson API 的 ObjectMapper,或者来自 JDBC API 的 DataSource 组件呢?您不能简单地编辑 JDK 中的代码并在标准 API 的类上添加 @Bean 注解。这就是 @Component 注解的用武之地。
如果您希望 Spring 管理一个您无法控制其代码的 JavaBean,您可以创建一个返回该 JavaBean 实例的方法,然后用 @Component 注解装饰该方法,如下例所示:

@Configuration
public class MyConfig {
    @Component
    public DataSource getMyHikariDataSource() {
        HikariDataSource ds = new HikariDataSource();
        ds.setJdbcUrl("jdbc:h2:mem:roshambo");
        return ds;
    }
    @Component
    public ObjectMapper getMyObjectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.enable(SerializationFeature.INDENT_OUTPUT);
        return mapper;
    }
}

在此示例中,我们使用了 @Component 注解来告诉 Spring IoC 容器管理 DataSourceObjectMapper bean 的生命周期。
这些组件来自 Jackson 和 JDBC API,因此我们无法编辑其源代码。这就是为什么我们不能直接在类声明上方添加 @Bean 注解的原因。但是,我们可以使用 @Component 注解,并结合放在类文件本身的 @Configuration 注解,来告诉 Spring 管理这些外部提供的资源。
用 @Component 代替 @Bean?
@Component 注解并不仅限于与外部 API 一起使用。开发者完全允许使用 @Component 注解代替 @Bean 注解来暴露他们自己编写的 JavaBeans。
如果我们从上方的 Score 类中移除 @Bean 注解,我们可以像下面代码中看到的那样,通过使用 @Component 注解来通过 IoC 容器暴露 Score

@Configuration
public class MyRoshamboConfig {
    @Component
    public Score getTheScore() {
        return new Score();
    }
}

何时使用 @Component vs @Bean?
在具有一定规模的 Spring Boot 项目中,我实际上更倾向于使用 @Component 注解而不是 @Bean 注解。这样,配置被限制在单个文件中,而您编写的 JavaBeans 不会被那些将您的源代码紧密绑定到 Spring 框架的注解所充斥。
在较小的项目和原型中?我完全支持使用 @Bean 注解。它更容易使用,并且如果您的项目不需要大量配置,它可以帮助您更快地启动和运行您的微服务。


【注】本文译自:Component vs. Bean annotations in Spring

Java中的多态与继承

Java中的多态与继承

开始学习Java中的多态及如何在多态方法调用中进行方法调用

多态——即对象根据其类型执行特定操作的能力——是Java代码灵活性的核心。四人组(Gang Of Four)创建的许多设计模式都依赖于某种形式的多态,包括命令模式。本文将介绍Java多态的基础知识及如何在程序中使用它。

关于Java多态需要了解的内容

  • 多态与Java继承
  • 为何多态重要
  • 方法重写中的多态
  • 核心Java类中的多态
  • 多态方法调用与类型转换
  • 保留关键字与多态
  • 多态的常见错误
  • 关于多态需要记住的要点

多态与Java继承

我们将重点探讨多态与Java继承的关系。需记住的核心点是:多态需要继承或接口实现。以下示例通过Duke和Juggy展示这一点:

public abstract class JavaMascot {
    public abstract void executeAction();
}

public class Duke extends JavaMascot {
    @Override
    public void executeAction() {
        System.out.println("Punch!");
    }
}

public class Juggy extends JavaMascot {
    @Override
    public void executeAction() {
        System.out.println("Fly!");
    }
}

public class JavaMascotTest {
    public static void main(String... args) {
        JavaMascot dukeMascot = new Duke();
        JavaMascot juggyMascot = new Juggy();
        dukeMascot.executeAction();
        juggyMascot.executeAction();
    }
}

代码输出为:

Punch!
Fly!

由于各自的具体实现,Duke和Juggy的动作均被执行。

为何多态重要

使用多态的目的是将客户端类与实现代码解耦。客户端类通过接收具体实现来执行所需操作,而非硬编码。这种方式下,客户端类仅需了解执行操作的必要信息,这是松耦合的典范。

为了更好地理解多态的优势,请观察以下SweetCreator

public abstract class SweetProducer {
    public abstract void produceSweet();
}

public class CakeProducer extends SweetProducer {
    @Override
    public void produceSweet() {
        System.out.println("Cake produced");
    }
}

public class ChocolateProducer extends SweetProducer {
    @Override
    public void produceSweet() {
        System.out.println("Chocolate produced");
    }
}

public class CookieProducer extends SweetProducer {
    @Override
    public void produceSweet() {
        System.out.println("Cookie produced");
    }
}

public class SweetCreator {
    private List<SweetProducer> sweetProducer;

    public SweetCreator(List<SweetProducer> sweetProducer) {
        this.sweetProducer = sweetProducer;
    }

    public void createSweets() {
        sweetProducer.forEach(sweet -> sweet.produceSweet());
    }
}

public class SweetCreatorTest {
    public static void main(String... args) {
        SweetCreator sweetCreator = new SweetCreator(
            Arrays.asList(
                new CakeProducer(),
                new ChocolateProducer(),
                new CookieProducer()
            )
        );
        sweetCreator.createSweets();
    }
}

此例中,SweetCreator类仅知晓SweetProducer类,而不了解每个甜点的具体实现。这种分离使类能灵活更新和重用,并大幅提升代码可维护性。设计代码时,应始终寻求使其尽可能灵活和可维护。多态是编写可重用Java代码的强力技术。

提示@Override注解强制程序员使用必须被重写的相同方法签名。若方法未被重写,将产生编译错误。

方法重载是多态吗?

许多程序员对多态与方法重写、重载的关系感到困惑。但只有方法重写是真正的多态。重载共享相同方法名但参数不同。多态是广义术语,因此相关讨论将持续存在。

方法重写中的多态

若返回类型是协变类型,则允许修改重写方法的返回类型。协变类型本质上是返回类型的子类。示例如下:

public abstract class JavaMascot {
    abstract JavaMascot getMascot();
}

public class Duke extends JavaMascot {
    @Override
    Duke getMascot() {
        return new Duke();
    }
}

由于DukeJavaMascot的子类,我们可在重写时修改返回类型。

核心Java类中的多态

我们在核心Java类中频繁使用多态。一个简单示例是实例化ArrayList类时声明List接口为类型:

List<String> list = new ArrayList<>();

进一步观察以下未使用多态的Java集合API代码:

public class ListActionWithoutPolymorphism {
    // 无多态的示例
    void executeVectorActions(Vector<Object> vector) {/* 此处代码重复 */}
    void executeArrayListActions(ArrayList<Object> arrayList) {/* 此处代码重复 */}
    void executeLinkedListActions(LinkedList<Object> linkedList) {/* 此处代码重复 */}
    void executeCopyOnWriteArrayListActions(CopyOnWriteArrayList<Object> copyOnWriteArrayList)
    { /* 此处代码重复 */}
}

public class ListActionInvokerWithoutPolymorphism {
    listAction.executeVectorActions(new Vector<>());
    listAction.executeArrayListActions(new ArrayList<>());
    listAction.executeLinkedListActions(new LinkedList<>());
    listAction.executeCopyOnWriteArrayListActions(new CopyOnWriteArrayList<>());
}

这段代码很糟糕,不是吗?想象维护它的难度!现在观察使用多态的相同示例:

public static void main(String … polymorphism) {
    ListAction listAction = new ListAction();    
    listAction.executeListActions();
}
public class ListAction {
    void executeListActions(List<Object> list) {
        // 对不同列表执行操作
    }
}
public class ListActionInvoker {
    public static void main(String... masterPolymorphism) {
        ListAction listAction = new ListAction();
        listAction.executeListActions(new Vector<>());
        listAction.executeListActions(new ArrayList<>());
        listAction.executeListActions(new LinkedList<>());
        listAction.executeListActions(new CopyOnWriteArrayList<>());
    }
}

多态的优势在于灵活性和扩展性。我们无需创建多个不同方法,只需声明一个接收通用List类型的方法。

多态方法调用与类型转换

可以在多态调用中调用特定方法,但会牺牲灵活性。示例如下:

public abstract class MetalGearCharacter {
    abstract void useWeapon(String weapon);
}
public class BigBoss extends MetalGearCharacter {
    @Override
    void useWeapon(String weapon) {
        System.out.println("Big Boss is using a " + weapon);
    }
    void giveOrderToTheArmy(String orderMessage) {
        System.out.println(orderMessage);
    }
}
public class SolidSnake extends MetalGearCharacter {
    void useWeapon(String weapon) {
        System.out.println("Solid Snake is using a " + weapon);
    }
}
public class UseSpecificMethod {
    public static void executeActionWith(MetalGearCharacter metalGearCharacter) {
        metalGearCharacter.useWeapon("SOCOM");
        // 以下行无法工作
        // metalGearCharacter.giveOrderToTheArmy("Attack!");
        if (metalGearCharacter instanceof BigBoss) {
            ((BigBoss) metalGearCharacter).giveOrderToTheArmy("Attack!");
        }
    }
    public static void main(String... specificPolymorphismInvocation) {
        executeActionWith(new SolidSnake());
        executeActionWith(new BigBoss());
    }
}

此处使用的技术是类型转换(casting),即在运行时显式改变对象类型。

注意:只有将通用类型强制转换为具体类型后,才能调用特定方法。这相当于明确告诉编译器:“我知道自己在做什么,因此要将对象转换为具体类型并使用特定方法。”

在上述示例中,编译器拒绝接受特定方法调用的原因很重要:传入的类可能是SolidSnake。在此情况下,编译器无法确保每个MetalGearCharacter的子类都声明了giveOrderToTheArmy方法。

保留关键字

注意保留字instanceof。在调用特定方法前,我们需检查MetalGearCharacter是否为BigBoss的实例。若BigBoss实例,将收到以下异常信息:

Exception in thread "main" java.lang.ClassCastException: com.javaworld.javachallengers.polymorphism.specificinvocation.SolidSnake cannot be cast to com.javaworld.javachallengers.polymorphism.specificinvocation.BigBoss

若需引用Java超类的属性或方法,可使用保留字super。例如:

public class JavaMascot {
    void executeAction() {
        System.out.println("The Java Mascot is about to execute an action!");
    }
}
public class Duke extends JavaMascot {
    @Override
    void executeAction() {
        super.executeAction();
        System.out.println("Duke is going to punch!");
    }
    public static void main(String... superReservedWord) {
        new Duke().executeAction();
    }
}

在Duke的executeAction方法中使用super可调用超类方法,再执行Duke的特定动作。因此输出如下:

The Java Mascot is about to execute an action!
Duke is going to punch!

多态的常见错误

  • 常见错误是认为无需类型转换即可调用特定方法。
  • 另一个错误是在多态实例化类时不确认将调用哪个方法。需记住:被调用的方法是所创建实例的方法。
  • 还需注意方法重写不同于方法重载
  • 若参数不同,则无法重写方法。若返回类型是超类方法的子类,则可以修改重写方法的返回类型。

关于多态需要记住的要点

  • 所创建的实例将决定使用多态时调用哪个方法。
  • @Override注解强制程序员使用重写方法;否则将产生编译错误。
  • 多态可用于普通类、抽象类和接口。
  • 大多数设计模式依赖某种形式的多态。
  • 调用多态子类中特定方法的唯一方式是使用类型转换。
  • 可通过多态设计强大的代码结构。

接受Java多态挑战!

让我们测试你对多态和继承的理解。在此挑战中,你需要根据Matt Groening的辛普森一家代码推断每个类的输出。首先仔细分析以下代码:

public class PolymorphismChallenge {
    static abstract class Simpson {
        void talk() {
            System.out.println("Simpson!");
        }
        protected void prank(String prank) {
            System.out.println(prank);
        }
    }
    static class Bart extends Simpson {
        String prank;
        Bart(String prank) { this.prank = prank; }
        protected void talk() {
            System.out.println("Eat my shorts!");
        }
        protected void prank() {
            super.prank(prank);
            System.out.println("Knock Homer down");
        }
    }
    static class Lisa extends Simpson {
        void talk(String toMe) {
            System.out.println("I love Sax!");
        }
    }
    public static void main(String... doYourBest) {
        new Lisa().talk("Sax :)");
        Simpson simpson = new Bart("D'oh");
        simpson.talk();
        Lisa lisa = new Lisa();
        lisa.talk();
        ((Bart) simpson).prank();
    }
}

你认为最终输出是什么?不要使用IDE!重点是提升代码分析能力,请自行推断结果。

选项:
A)

I love Sax!  
 D'oh  
 Simpson!  
 D'oh  

B)

Sax :)  
 Eat my shorts!  
 I love Sax!  
 D'oh  
 Knock Homer down  

C)

Sax :)  
 D'oh  
 Simpson!  
 Knock Homer down  

D)

I love Sax!  
 Eat my shorts!  
 Simpson!  
 D'oh  
 Knock Homer down

解答挑战
对于以下方法调用:

new Lisa().talk("Sax :)");

输出为“I love Sax!”,因为我们向方法传递了字符串且Lisa类有此方法。

下一调用:

Simpson simpson = new Bart("D'oh");
simpson.talk();

输出为“Eat my shorts!”,因为我们用Bart实例化了Simpson类型。

以下调用较为复杂:

Lisa lisa = new Lisa();
lisa.talk();

此处通过继承使用了方法重载。由于未向talk方法传递参数,因此调用Simpsontalk方法,输出为:

"Simpson!"

最后一个调用:

((Bart) simpson).prank();

此例中,prank字符串在实例化Bart时通过new Bart("D'oh")传入。此时首先调用super.prank方法,再执行Bart的特定prank方法。输出为:

"D'oh"
"Knock Homer down"

因此正确答案是D。输出为:

I love Sax!
Eat my shorts! 
Simpson!
D'oh
Knock Homer down

【注】本文译自:Polymorphism and inheritance in Java | InfoWorld

Netflix系统架构解析

Netflix系统架构解析

Netflix架构旨在高效可靠地同时为数百万用户提供内容。以下是其特性和组件的详细分析。


是否曾好奇Netflix如何让您目不转睛地享受无中断的流畅播放体验?幕后功臣正是Netflix架构,它负责提供吸引全球观众的无缝流媒体体验。Netflix的系统架构强调了决定未来内容形态的重要性。让我们一起探索Netflix流媒体宇宙的幕后故事!
Netflix已成为娱乐、追剧和尖端流媒体服务的代名词。其迅速崛起可归因于庞大的内容库、全球覆盖以及弹性创新的架构。
从1997年的DVD租赁服务发展为全球流媒体巨头,Netflix始终运用前沿技术革新着媒体消费方式。
Netflix架构旨在高效可靠地同时为数百万用户提供内容。鉴于其在190多个国家拥有超过2亿会员,基础设施的可扩展性至关重要。
让我们深入探究Netflix架构的复杂性,揭示其如何持续塑造我们享受喜爱节目的方式。

理解Netflix系统架构的重要性

理解Netflix系统架构至关重要,原因包括:
首先,它揭示了Netflix如何为全球数百万用户提供无瑕疵的流媒体体验。通过探索架构细节,我们能更好地理解其成功背后的技术与策略。
此外,其他行业可将Netflix设计作为开发可扩展、可靠且高效系统的蓝图。其设计原则和最佳实践为构建复杂分布式系统提供了重要经验。
理解Netflix架构还能让我们认识到推动数字媒体消费发展的持续创新。

理解系统设计需求

系统设计对开发复杂软件或技术基础设施至关重要。这些规范是构建整个系统的基础,驱动决策并塑造最终产品。那么系统设计的先决条件是什么?为何如此重要?让我们进行探讨。

功能性需求

功能性需求规定了系统必须包含的功能和能力。这些规范概述系统主要目标,并详述各部件如何交互。以Netflix为例的流媒体平台功能性需求包括但不限于:

  1. 账户创建: 用户应能轻松创建账户,提供注册所需信息。
  2. 用户登录: 注册用户应能通过认证凭证安全登录。
  3. 内容推荐: 平台应根据用户偏好和观看历史提供个性化建议。
  4. 视频播放: 用户应能无缝播放视频,支持播放控制功能。

非功能性需求

非功能性需求定义系统在不同场景下的行为,确保满足特定质量要求。涵盖性能、可扩展性、可靠性、安全性和合规性等方面。以Netflix为例包括但不限于:

  1. 性能需求: 高负载时保持低延迟和高吞吐量。
  2. 合规需求: 遵守数据保护法规标准。
  3. 扩展性需求: 基础设施需支持用户增长而不影响性能。
  4. 安全需求: 实施强认证和加密防止未授权访问。
  5. 可靠性需求: 包含故障转移方法并保证高正常运行时间。

Netflix架构:拥抱云原生

2008年8月因数据库损坏遭遇重大挫折后,Netflix得出关键结论:必须摆脱单点故障,转向高可靠、水平可扩展的云解决方案。Netflix选择AWS作为云供应商,2015年将多数服务迁移至云端。经过七年努力,2016年1月初完成云迁移,关闭了最后的数据中心组件。
上云并非易事。Netflix采用云原生策略,彻底改革运营模式和技术栈:采用NoSQL数据库、反规范化数据模型、从单体应用转向数百个微服务。文化变革也不可或缺,如采用DevOps流程、持续交付和自助式工程环境。尽管困难重重,此转型使Netflix成为云原生企业,为在线娱乐领域的未来扩展和创新奠定基础。

Netflix架构三要素

由客户端、后端和内容分发网络(CDN)构成的强大架构三要素,共同保障无瑕疵用户体验。面对全球数百万观众,每个组件对内容交付都至关重要。

客户端

客户端架构是Netflix体验的核心,涵盖用户访问的各种设备(电脑、智能电视、智能手机)。Netflix混合使用Web界面和原生应用确保跨平台一致体验。无论设备类型,这些客户端管理播放控制、用户交互与界面渲染,提供统一体验。得益于响应式优化,用户可轻松浏览内容库并享受连续播放。

后端架构

后端架构是幕后运营的支柱。用户账户管理、内容目录、推荐算法、计费系统等由复杂的服务器、数据库和微服务网络处理。
后端不仅处理用户数据与内容交付,还运用大数据分析和机器学习优化内容交付与个性化推荐,提升用户满意度。
Netflix后端架构历经重大演变:2007年迁移至云基础设施,2018年采用Spring Boot作为主要Java框架。结合AWS的可扩展性和可靠性,Ribbon、Eureka和Hystrix等专有技术有效协调后端运营。

内容分发网络(CDN)

CDN完善架构三角。Netflix运营名为Open Connect的CDN,通过战略部署的全球服务器网络,以最高可靠性和最小延迟交付内容。
通过在靠近用户的站点缓存内容,减少缓冲并确保流畅播放。即使在高峰期,通过全球服务器分发内容减少拥塞并最大化带宽利用率。这种去中心化方式提升全球观看体验,降低缓冲时间并提高流媒体质量。

客户端组件

Web界面

近年Netflix Web界面经历重大转型,从Silverlight转向HTML5流式传输视频内容。此举消除了浏览器插件需求,简化用户体验。自采用HTML5后,提升了对Chrome、Safari、Firefox等浏览器的兼容性。
Netflix对HTML5的应用不仅限于基础播放,还借此支持行业标准与技术进步。

移动应用

通过iOS和Android应用将流媒体体验延伸至移动用户。结合原生开发与平台优化,为各类移动设备提供流畅界面。
凭借个性化推荐、无缝播放和离线下载等功能,满足移动观众需求。用户可随时随地观看喜爱的内容,Netflix通过频繁升级提供引人入胜的移动体验。

智能电视应用

电视应用基于复杂架构,包含Gibbon渲染层、动态更新的JavaScript应用和原生SDK。通过定制版React-Gibbon确保跨电视平台的流畅UI渲染与响应。
性能优化聚焦每秒帧数与输入响应等指标,通过减少属性迭代等方法提升渲染效率,样式优化与自定义组件开发进一步优化性能。

重塑播放体验:现代化之旅

过去十年Netflix彻底改变了数字媒体消费方式。尽管持续推出创新功能,但自2013年以来播放界面的视觉设计与用户控制鲜有变化。认识到需要更新后,Web UI团队着手重新设计。
团队聚焦三大画布:播放前、视频播放和播放后,目标是提升客户满意度。通过React.js和Redux等技术加速开发与提升性能,革新了播放界面。

后端基础设施

内容分发网络(CDN)

Netflix基础设施依赖Open Connect CDN,轻松向全球数百万观众交付内容。全球分布的CDN对确保各地高质量流媒体至关重要。
通过名为OCA的服务器战略部署于ISP和用户附近,在高峰期降低延迟并保障性能。通过在ISP网络预置内容,最大化带宽利用率并减少对骨干网络的依赖。
可扩展性是CDN的核心特性。全球约1000个地点部署OCA(包括偏远地区),满足各地增长需求。
向合格ISP提供OCA,使其直接从自身网络提供内容,既提升质量又降低ISP成本,建立双赢关系。

视频处理转型:微服务革命

通过实施微服务改造视频处理流水线,实现无与伦比的可扩展性和灵活性。从单体平台转向微服务平台开启了敏捷性和功能开发速度的新纪元。
视频处理流程的每一步由独立微服务代表,实现简化编排与解耦功能。从视频检测到编码,这些服务共同产出优质视频资产。微服务通过快速迭代适应业务需求变化,取得显著成效。

Open Connect播放流程

全球客户能够享受丝滑无暇的观看体验得益于Netflix Open Connect 的播放流程。其运作方式如下:

  1. 健康状态报告: 开放连接设备(OCAs)定期向亚马逊云服务(AWS)中的缓存控制服务汇报其学习到的路由信息、内容可用性及整体运行状况。
  2. 用户请求: 用户通过客户端设备上托管在AWS的Netflix应用程序请求播放电视剧或电影。
  3. 授权与文件选择: 在验证用户授权和许可后,AWS播放应用程序服务会精确选择处理播放请求所需的文件。
  4. 导向服务: AWS导向服务根据缓存控制服务保存的数据,选择用于提供文件的OCA设备。播放应用程序服务从导向服务获取这些OCA设备信息并构建其URL地址。
  5. 内容传输: 播放应用程序服务将相关OCA的URL发送至客户端设备。当请求的文件通过HTTP/HTTPS协议传输至客户端时,选定的OCA设备即开始提供服务。

下方图示展示了完整的播放流程:

数据库架构

利用Amazon S3实现无缝媒体存储

Netflix在2022年4月21日AWS服务中断期间的表现,充分证明了其云基础设施的价值,特别是对Amazon S3数据存储服务的依赖。通过整合SimpleDB、S3和Cassandra等服务,Netflix构建了能够承受此类中断的健壮系统。
作为基础设施的核心支柱,Netflix采用Amazon S3(简单存储服务)存储海量影视内容与原创作品。为服务全球数亿用户,平台需要管理PB级数据,而S3提供的可扩展、高可靠且易访问的存储特性成为理想选择。
内容库持续扩张时,S3使Netflix无需担忧硬件扩容或复杂存储架构维护,完美契合其"不牺牲用户体验"的扩展需求。

拥抱NoSQL实现弹性扩展

面对分布式架构的结构化存储需求,Netflix在发现传统关系型数据库的局限性后,全面转向NoSQL分布式数据库。技术栈中Cassandra, Hadoop/HBase, 和SimpleDB三大核心方案各具优势。

Amazon SimpleDB

迁移至AWS云时,SimpleDB凭借强大的查询能力、跨可用区自动复制和高持久性成为首选。其托管特性有效降低了运维成本,符合Netflix将非核心业务外包给云服务商的策略。

Apache HBase

作为Hadoop生态的高性能解决方案,HBase通过动态分区策略实现负载均衡与集群扩展,完美应对Netflix的数据增长挑战。分布式计数器、范围查询和数据压缩等功能,进一步强化了其一致性架构的健壮性。

Apache Cassandra

这款开源NoSQL数据库以性能、弹性和灵活性见长。动态集群扩展能力满足Netflix无限扩容需求,自适应一致性机制与灵活数据模型使其成为跨区域部署、避免单点故障的理想选择。
虽然需要面对学习曲线和运维成本,但NoSQL在可扩展性、可用性和性能方面的优势,使其成为Netflix长期云战略的关键支柱。

计费系统中的MySQL实践

Netflix计费系统作为向AWS云原生架构全面迁移的一部分经历了重大转型。由于Netflix运营高度依赖计费系统,此次迁移被谨慎处理以确保对会员体验的影响最小化,同时严格遵守严格的财务标准。
跟踪计费周期、监控支付状态以及向财务系统提供报告数据只是Netflix计费基础设施处理的众多任务中的几项。计费工程团队管理着一个包含批处理任务、API、与其他服务的连接器以及数据管理的复杂生态系统来实现这些功能。
数据库技术的选择是迁移过程中最重要的决策之一。由于支付处理需要可扩展性和ACID事务支持,MySQL被选为数据库解决方案。
构建健壮的工具链、优化代码和清理不必要数据都是迁移过程的一部分,以适应新的云架构。在转移现有会员数据前,团队使用代理和重定向器处理流量重定向,并采用干净数据集进行了全面测试流程。
将计费系统迁移至AWS上的MySQL是个复杂过程,需要周密规划、系统实施以及持续测试和迭代。尽管存在困难,迁移最终顺利完成,使Netflix能够利用AWS云服务的可扩展性和可靠性来支持其计费系统。
总之,将Netflix计费系统切换至AWS上的MySQL涉及大量工程工作并产生广泛影响。Netflix的系统架构已更新其计费系统,并采用基于云的解决方案为数字领域的未来发展做好准备。
以下是Netflix迁移后的架构:

Netflix架构中的内容处理流水线

Netflix内容处理流水线是处理内容合作伙伴提供的数字资产的系统化方法。主要包含三个阶段:内容摄取、转码和封装。

内容摄取

在摄取阶段,音频、定时文本或视频等源文件会经过严格的准确性和合规性检查。这些验证包括:语义信号域检查、文件格式验证、压缩码流可解码性验证、符合Netflix交付标准以及数据传输完整性检查。

转码与封装

通过摄取阶段的源文件会进行转码处理,生成输出基本流。随后这些流会被加密并封装至可分发的流式容器中。

通过Netflix金丝雀模型确保无缝流媒体体验

由于客户端应用是用户与品牌互动的主要方式,它们必须保持卓越品质。Netflix系统架构投入大量资源确保对更新版本进行全面评估。然而,由于Netflix需要在数千种设备上运行,并依赖数百个独立部署的微服务,全面内部测试变得困难。因此,必须依靠更新过程中获取的可靠现场数据来支持发布决策。
为加速客户端应用更新评估,Netflix系统架构组建了专门团队从现场挖掘健康信号。这项系统投资提高了开发速度,改善了应用质量和开发流程。

  1. 客户端应用: Netflix通过两种方式更新客户端应用:直接下载和应用商店部署。直接下载提高了分发控制力。
  2. 部署策略: 虽然定期增量发布的优势众所周知,但软件更新仍存在挑战。由于每个用户设备都以流形式传输数据,高效信号采样至关重要。Netflix采用定制部署策略应对各类设备和复杂微服务的独特挑战。策略因客户端类型而异(如智能电视与移动应用)。新版本通过分阶段发布逐步推出,提供快速故障处理和智能后端服务扩展。发布过程中监控客户端错误率和采用率可确保部署的一致性和有效性。
  3. 分阶段发布: 为降低风险并合理扩展后端服务,分阶段发布需要逐步部署新版本。
  4. AB测试/客户端金丝雀: Netflix采用强化的A/B测试变体"客户端金丝雀",通过完整应用测试确保数小时内完成及时更新。
  5. 编排: 编排减少了频繁部署和分析的工作量,有效管理A/B测试和客户端金丝雀。

总之,得益于Netflix采用客户端金丝雀模型,数百万用户能享受无瑕疵的流媒体体验,该模型确保了应用的频繁更新。

Netflix架构图示

Netflix系统架构是一个复杂生态系统:后端服务采用Python和Java(Spring Boot),数据处理和实时事件流使用Apache Kafka和Flink。前端采用Redux、React.js和HTML5提供沉浸式用户体验。多种数据库(包括Cassandra、HBase、SimpleDB、MySQL和Amazon S3)提供实时分析并处理海量媒体内容。Jenkins和Spinnaker实现持续集成和部署,AWS为整个基础设施提供可扩展性、可靠性和全球覆盖能力。
这些技术仅占Netflix庞大技术栈的一小部分,体现了其为全球观众提供完美娱乐体验的决心。

Netflix架构总结

Netflix系统架构彻底改变了娱乐行业。从DVD租赁服务发展为全球流媒体巨头,其技术基础设施是成功的关键。
依托AWS支持的Netflix架构确保全球用户的无中断流媒体体验,通过客户端、后端和内容分发网络(CDN)实现跨设备的无瑕疵内容传输。
HTML5的创新应用和个性化推荐提升了用户体验。
尽管面临挑战,向云原生架构的转型使Netflix更加强大。通过采用微服务、NoSQL数据库和云解决方案,Netflix在快速发展的在线娱乐领域为未来创新做好准备。任何技术企业都能从理解Netflix系统中获益。
简而言之,Netflix系统架构不仅关乎技术,更旨在改变我们的媒体消费方式。当观众追剧时,这套架构在幕后确保一切顺畅运行,提升每个人的娱乐享受。


【注】本文译自: A Look Into Netflix System Architecture

如何在Java程序中使用泛型

如何在Java程序中使用泛型

泛型可以使你的代码更灵活、更易读,并能帮助你在运行时避免ClassCastExceptions。让我们通过这篇结合Java集合框架的泛型入门指南,开启你的泛型之旅。

Java 5引入的泛型增强了代码的类型安全性并提升了可读性。它能帮助你避免诸如ClassCastException(当尝试将对象强制转换为不兼容类型时引发的异常)这类运行时错误。

本教程将解析泛型概念,通过三个结合Java集合框架的实例演示其应用。同时我们将介绍原始类型(raw types),探讨选择使用原始类型而非泛型的场景及其潜在风险。

Java编程中的泛型

  • 为何使用泛型?
  • 如何利用泛型保障类型安全
  • Java集合框架中的泛型应用
  • Java泛型类型示例
  • 原始类型与泛型对比

为何使用泛型?

泛型在Java集合框架中被广泛用于java.util.List、java.util.Set和java.util.Map等接口。它们也存在于Java其他领域,如java.lang.Class、java.lang.Comparable 和java.lang.ThreadLocal。

在泛型出现前,Java代码常缺乏类型安全保障。以下是非泛型时代Java代码的典型示例:

List integerList = new ArrayList();
integerList.add(1);
integerList.add(2);
integerList.add(3);

for (Object element : integerList) {
    Integer num = (Integer) element; // 必须显式类型转换
    System.out.println(num);
}

这段代码意图存储Integer对象,但没有任何机制阻止你添加其他类型(如字符串):

integerList.add("Hello");

当尝试将String强制转换为Integer时,这段代码会在运行时抛出ClassCastException。

利用泛型保障类型安全

为解决上述问题并避免ClassCastExceptions,我们可以使用泛型指定列表允许存储的对象类型。此时无需手动类型转换,代码更安全且更易理解:

List<Integer> integerList = new ArrayList<>();

integerList.add(1);
integerList.add(2);
integerList.add(3);

for (Integer num : integerList) {
    System.out.println(num);
}

List表示"存储Integer对象的列表"。基于此声明,编译器确保只有Integer对象能被添加至列表,既消除了类型转换需求,也预防了类型错误。

Java集合框架中的泛型

泛型深度集成于Java集合框架,提供编译时类型检查并消除显式类型转换需求。当使用带泛型的集合时,你需指定集合可容纳的元素类型。Java编译器基于此规范确保你不会意外插入不兼容对象,从而减少错误并提升代码可读性。

为演示泛型在Java集合框架中的使用,让我们观察几个实例。

List和ArrayList的泛型应用

前例已简要展示ArrayList的基本用法。现在让我们通过List接口的声明深入理解这一概念:

public interface List<E> extends SequencedCollection<E> { … }

此处声明泛型变量为"E",该变量可被任何对象类型替代。注意变量E代表元素(Element)。

接下来演示如何用具体类型替换变量。下例中将替换为

List<String> list = new ArrayList<>();
list.add("Java");
list.add("Challengers");
// list.add(1); // 此行会导致编译时错误

List声明该列表仅能存储String对象。如代码最后一行所示,尝试添加Integer将引发编译错误。

Set和HashSet的泛型应用

Set接口与List类似:

public interface Set<E> extends Collection<E> { … }

我们将用替换,使集合只能存储Double值:

Set<Double> doubles = new HashSet<>();
doubles.add(1.5);
doubles.add(2.5);
// doubles.add("three"); // 编译时错误

double sum = 0.0;
for (double d : doubles) {
    sum += d;
}

Set确保只有Double值能被添加至集合,防止因错误类型转换引发的运行时错误。

Map和HashMap的泛型应用

我们可以声明任意数量的泛型类型。以键值数据结构Map为例,K代表键(Key),V代表值(Value):

public interface Map<K, V> { … }

现在用String替换K作为键类型,用Integer替换V作为值类型:

Map<String, Integer> map = new HashMap<>();
map.put("Duke", 30);
map.put("Juggy", 25);
// map.put(1, 100); // 此行会导致编译时错误

此例展示将String键映射到Integer值的HashMap。添加Integer类型的键将不被允许并导致编译错误。

泛型命名规范

我们可以在任何类中声明泛型类型。虽然可以使用任意名称,但建议遵循命名规范:

  • E 代表元素(Element)
  • K 代表键(Key)
  • V 代表值(Value)
  • T 代表类型(Type)

应避免使用无意义的"X"、"Y"或"Z"等名称。

Java泛型类型使用示例

现在通过更多示例深入演示Java中泛型类型的声明与使用。

创建通用对象容器

我们可以在自定义类中声明泛型类型,不必局限于集合类型。下例中,Box类通过声明泛型类型E来操作任意元素类型。注意泛型类型E声明于类名之后,随后即可作为属性、构造器、方法参数和返回类型使用:

// 定义带泛型参数E的Box类
public class Box<E> {
    private E content; // 存储E类型对象

    public Box(E content) { this.content = content; }
    public E getContent() { return content; }
    public void setContent(E content) { 
        this.content = content;
    }

    public static void main(String[] args) {
        // 创建存储Integer的Box
        Box<Integer> integerBox = new Box<>(123);
        System.out.println("整数盒内容:" + integerBox.getContent());

        // 创建存储String的Box
        Box<String> stringBox = new Box<>("Hello World");
        stringBox.setContent("Java Challengers");
        System.out.println("字符串盒内容:" + stringBox.getContent());
    }
}

输出结果:

整数盒内容:123
字符串盒内容:Java Challengers

代码要点:

  • Box类使用类型参数E作为容器存储对象的占位符,允许Box处理任意对象类型
  • 构造器初始化Box实例时接受指定类型对象,确保类型安全
  • getContent返回与实例创建时指定的泛型类型匹配的对象,无需类型转换
  • setContent通过类型参数E确保只能设置正确类型的对象
  • main方法创建了存储Integer和String的Box实例
  • 每个Box实例操作特定数据类型,展现泛型在类型安全方面的优势

此例展示了Java泛型的基础实现,演示了如何以类型安全方式创建和操作任意类型对象。

处理多数据类型

我们可以声明多个泛型类型。以下Pair类包含<K, V>泛型值。如需更多泛型参数,可扩展为<K, V, V1, V2, V3>等,代码仍可正常编译。

Pair类示例:

class Pair<K, V> {
    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K getKey() { return key; }
    public V getValue() { return value; }

    public void setKey(K key) { this.key = key; }
    public void setValue(V value) { this.value = value; }
}

public class GenericsDemo {
    public static void main(String[] args) {
        Pair<String, Integer> person = new Pair<>("Duke", 30);

        System.out.println("姓名:" + person.getKey());
        System.out.println("年龄:" + person.getValue());

        person.setValue(31);
        System.out.println("更新后年龄:" + person.getValue());
    }
}

输出结果:

姓名:Duke
年龄:30
更新后年龄:31

代码要点:

  • Pair<K, V>类包含两个类型参数,适用于任意数据类型组合
  • 构造器与方法使用类型参数实现严格类型检查
  • 创建存储String(姓名)和Integer(年龄)的Pair对象
  • 访问器和修改器方法操作Pair数据
  • Pair类可存储管理关联信息而不受特定类型限制,展现泛型的灵活性与强大功能

此例展示泛型如何创建支持多数据类型的可复用类型安全组件,提升代码复用性和可维护性。

让我们再看一个示例。

方法级泛型声明

泛型类型可直接在方法中声明,无需在类级别定义。若某个泛型类型仅用于特定方法,可在方法签名返回类型前声明:

public class GenericMethodDemo {

    // 声明泛型类型<T>并打印指定类型数组
    public static <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.print(element + " ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        Integer[] intArray = {1, 2, 3, 4};
        printArray(intArray);

        String[] stringArray = {"Java", "Challengers"};
        printArray(stringArray);
    }
}

输出结果:

1 2 3 4
Java Challengers

原始类型与泛型对比

原始类型指未指定类型参数的泛型类或接口名称。在Java 5引入泛型前,原始类型被广泛使用。现今开发者通常仅在与遗留代码兼容或与非泛型API交互时使用原始类型。即使使用泛型,仍需了解如何识别和处理原始类型。

典型原始类型示例——未指定类型参数的List声明:

 List rawList = new ArrayList();

此处List rawList声明了一个未指定泛型参数的列表。rawList可存储任意类型对象(Integer、String、Double等)。由于未指定类型,编译器不会对添加至列表的对象类型进行检查。

使用原始类型的编译警告

Java编译器会对原始类型使用发出警告,提醒开发者可能存在的类型安全隐患。当使用泛型时,编译器会检查集合(如List、Set)中存储的对象类型、方法返回类型和参数是否匹配声明类型,从而预防如ClassCastException的常见错误。

使用原始类型时,由于未指定存储对象类型,编译器无法进行类型检查,因此会发出警告提示你绕过了泛型提供的类型安全机制。

编译警告示例

以下代码演示编译器如何对原始类型发出警告:

List list = new ArrayList(); // 警告:原始使用参数化类'List'
list.add("hello");
list.add(1);

编译时通常会显示:

注意:SomeFile.java使用了未经检查或不安全的操作。
注意:使用-Xlint:unchecked重新编译以获取详细信息。

使用-Xlint:unchecked参数编译将显示更详细警告:

warning: [unchecked] unchecked call to add(E) as a member of the raw type List
    list.add("hello");
            ^
  where E is a type-variable:
    E extends Object declared in interface List

若确信使用原始类型不会引入风险,或处理无法重构的遗留代码,可使用@SuppressWarnings("unchecked")注解抑制警告。但需谨慎使用,避免掩盖真实问题。

使用原始类型的后果

尽管原始类型有助于向后兼容,但存在两大缺陷:类型安全性缺失和维护成本增加。

  • 类型安全性缺失:泛型的核心优势是类型安全,使用原始类型将丧失这一优势。编译器不进行类型正确性检查,可能导致运行时ClassCastException。
  • 维护成本增加:使用原始类型的代码缺乏泛型提供的明确类型信息,维护难度加大,易产生仅在运行时暴露的错误。

类型安全问题示例:使用原始类型List而非泛型List时,编译器允许添加任意类型对象。当从列表检索元素并尝试强制转换为String时,若实际为其他类型将导致运行时错误。

泛型知识要点回顾

泛型以高度灵活性提供类型安全保障。以下回顾关键要点:

泛型是什么?为何使用?

  • code.Java 5引入泛型以提升代码类型安全性和灵活性
  • 主要优势在于帮助避免ClassCastException等运行时错误
  • 泛型广泛应用于Java集合框架,也见于Class、Comparable、ThreadLocal等组件
  • 通过阻止不兼容类型插入实现类型安全

Java集合中的泛型

  • List和ArrayList:List允许指定元素类型E,确保列表类型专一
  • Set和HashSet:Set限定元素为类型E,保持一致性
  • Map和HashMap:Map<K,V>定义键值类型,提升类型安全性和代码清晰度

泛型使用优势

  • 通过阻止不兼容类型插入减少错误
  • 明确类型关联提升代码可读性和可维护性
  • 便于以类型安全方式创建和管理集合等数据结构

AI时代的非人类身份安全

AI时代的非人类身份安全

随着AI在企业中的崛起,攻击面也在不断扩展。了解如何保护非人类身份(Non-Human Identities, NHIs)并防止未经授权的访问。

AI时代的非人类身份安全


非人类身份(NHIs)近期成为焦点并非偶然——随着AI工具和自主代理的快速普及,企业的NHI数量正呈爆炸式增长。这一趋势也引发了关于机器身份与治理的大量研究和讨论。

与系统的普通用户类似,NHI(如AI代理、机器人、脚本和云工作负载)通过密钥(secrets)进行操作。这些凭证赋予其访问敏感系统和数据的权限,可能以多种形式存在,且必须从创建到销毁全程受控。然而,机器无法使用多因素认证或通行密钥(passkeys),而开发者在部署应用时可能生成数百个此类凭证。


AI加速NHI的扩张与风险

企业AI的采用速度惊人,迫使开发者以前所未有的速度推出NHI。AI虽能提升效率,但也带来隐私泄露、密钥暴露和不安全代码等风险。大型语言模型(LLMs)的应用场景令人兴奋,但需谨记:技术引入越多,攻击面越大——尤其是当AI代理被赋予自主权时。


AI代理带来的NHI风险

1. AI代理与密钥泛滥(Secrets Sprawl)

“AI代理”是基于LLM的系统,可自主决策如何完成任务。它们不同于传统的确定性机器人(仅按开发者预设的步骤执行),而是能访问内部数据源、搜索互联网,并代表用户与其他应用交互。

例如,一个AI采购代理可以分析需求、通过电商平台比价、与AI聊天机器人议价,甚至自主下单。每个安全通信都需要凭证,而这类代理需通过DevOps流程部署,导致更多跨环节的身份验证需求。密钥往往在系统、日志和仓库中意外散落。

企业常为AI代理赋予比传统机器人更广泛的读写、甚至创建和删除权限。由于AI代理的自主性,若权限限制过严,其任务可能受阻;但宽松权限又易导致过度授权。

风险点:任一密钥泄露都可能引发数据泄露或未经授权的交易。需通过最小权限访问、API密钥保护和审计日志来强化NHI治理,并关注密钥存储之外的暴露风险。

2. 孤立的API密钥(Orphaned API Keys)

孤立API密钥指不再与用户账户关联的密钥(如员工离职后未被删除的密钥)。在NHI场景中,密钥的“归属权”模糊(开发者?运维团队?),导致其极易被遗忘却仍有效。

关键问题:谁应对这些密钥引发的安全漏洞负责?

3. 基于提示的架构与敏感数据暴露

AI助手(如ChatGPT、Gemini、GitHub Copilot)依赖提示(prompt)架构,通过上下文、命令和数据与LLM交互。这种模式虽简化了开发,但也可能导致敏感信息(如API密钥)被写入提示或日志。

案例:财务团队用AI聊天机器人处理发票时,若提示中包含API key ABC123,该密钥可能被明文记录。若日志未加密,攻击者可借此入侵发票系统。

防护措施:需阻止开发者及用户将敏感数据嵌入提示或日志,并扫描LLM输出中的异常信息。

4. AI代理与数据收集风险

AI代理常从以下来源收集数据:

  • 云存储(如AWS S3、Google Drive)
  • 企业应用(如Jira、Confluence、Salesforce)
  • 通信系统(如Slack、Microsoft Teams)

风险:若AI代理可访问这些系统中的任何数据,攻击者亦可滥用其NHI权限。需定期轮换所有内部系统的密钥,并清理日志。

5. AI生成代码与嵌入式密钥

GitHub Copilot、Amazon CodeWhisperer等AI编码工具已被超50%的开发者使用。然而,AI生成的代码可能诱导开发者硬编码密钥(如API密钥、数据库凭证)。

案例:开发者要求Copilot生成调用云服务的代码时,可能得到:

import requests  
API_KEY = "sk_live_ABC123XYZ"  
response = requests.get("https://api.example.com/data", headers={"Authorization": f"Bearer {API_KEY}"})  

若匆忙中替换为真实密钥并提交至代码仓库,凭证可能被泄露。

防护:通过预提交钩子(pre-commit hooks)等工具扫描代码,防止密钥泄露。


未来方向:如何保护非人类身份

  1. 发现密钥:自动识别企业环境中的所有AI代理凭证(包括存储库内外)。
  2. 评估风险:明确密钥的用途、访问范围及关联的关键系统。
  3. 动态防护:实时监控提示和日志,防止敏感数据嵌入。

让NHI治理跟上AI速度

AI代理的部署速度与复杂性并存,既带来效率提升,也伴随风险。随着AI普及,保护机器身份已非可选,而是必需。唯有通过系统化的密钥管理、权限控制和持续监测,才能在AI时代实现安全与创新的平衡。


【注】本文译自:
Non-Human Identity Security in the Age of AI

Java Stream API:每个开发者都应该知道的 3 件事

Java Stream API

Java Stream API:每个开发者都应该知道的 3 件事

Java Stream API 通过惰性求值并行处理函数式编程简化了集合处理。使用它可以编写更简洁、高效和可扩展的代码。

时间飞逝!我记得 Java 8 曾经是一个标杆,每个人都把它当作一种全新且革命性的东西来谈论。老实说,它确实是全新且革命性的。但现在,使用 Java 8 的项目可能被称为“遗留”项目。即使 Java 8 本身已经成为遗留版本,它引入的特性仍然具有实际意义。今天,我们来聊聊其中一个特性——Stream API

如果你还不了解,Java Stream API 是一个强大的工具,它允许程序员以函数式编程风格编写 Java 代码。它通过支持过滤、转换和聚合操作,使得集合的处理更加简单。

尽管 Stream API 被广泛使用,但我仍然发现许多开发者对其深层知识的掌握存在不足。在本文中,我将探讨 Stream API 的三个关键方面,这些方面对于深入理解它至关重要:

  1. 惰性求值:帮助我们优化操作链的执行。
  2. 并行流:通过利用多核处理器,深入探讨如何增加数据处理的并行性。
  3. Lambda 变量作用域:了解在使用 Stream 时如何正确地将变量传递给 Lambda。

我希望通过本文,你能更好地理解这些概念。


1. Java Stream API 中的惰性求值

惰性求值是理解如何有效使用流的核心概念。但在深入探讨惰性求值之前,我们先来了解流管道中的两种主要操作类型:中间操作终端操作

  1. 中间操作:中间操作是将输入流转换为另一个流的操作,但不会产生不可变的结果。例如,filter()map()flatMap() 都是中间操作,因为它们接受一个输入流并返回另一个流。这些操作不会立即消耗输入流中的所有元素,而是创建一个包含所需元素的新流。

  2. 终端操作:终端操作是消耗流中元素的操作,它们要么返回一个结果,要么通过副作用修改某些状态。例如,forEach()findFirst()collect() 都是终端操作,因为它们最终会消耗流中的所有元素以产生结果。

什么是惰性求值?它是如何工作的?

在 Stream API 中,惰性求值意味着中间操作不会立即执行,直到我们调用一个终端操作。这意味着我们可以在代码的任何地方定义一个流及其所有操作,但只有在调用终端操作时才会执行。

当我们调用终端操作时,流会逐个处理数据元素,依次应用所有中间操作。这种方法通过避免不必要的计算来优化性能。

让我们通过一个实际例子来看看惰性求值如何影响执行:

import java.util.stream.Stream;

public class LazyEvaluationExample {
    public static void main(String[] args) {
        Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5)
            .filter(num -> {
                System.out.println("Filtering: " + num);
                return num % 2 == 0;
            })
            .map(num -> {
                System.out.println("Mapping: " + num);
                return num * 2;
            });

        System.out.println("Stream pipeline defined, no execution yet.");

        // 终端操作触发执行
        stream.forEach(System.out::println);
    }
}

输出:

Stream pipeline defined, no execution yet.
Filtering: 1
Filtering: 2
Mapping: 2
4
Filtering: 3
Filtering: 4
Mapping: 4
8
Filtering: 5

让我们试着理解为什么会有这样的输出。我们可以看到,filter()map() 操作是惰性的。这意味着即使我们编写了这些代码,它们也不会在调用终端操作 forEach() 之前执行。这解释了为什么我们首先看到输出 Stream pipeline defined, no execution yet

只有当调用 forEach() 终端操作时,流才会开始逐个处理元素。


2. 并行流

Java Stream API 最有用和最强大的功能之一是对并行流的支持。并行性是指通过利用多个 CPU 核心同时处理两个或多个操作的能力。在 Stream API 中,这意味着我们可以同时处理流中的多个元素的中间或终端操作。

这种功能可以显著提高计算密集型任务的性能,但为了更好地理解它以达到最佳效果,我们需要深入了解它。

什么是并行流?

并行流是一种将其元素分成多个块,然后通过不同线程并行处理的流。与普通流(逐个处理元素)不同,并行流在底层使用 ForkJoinPool 来实现并行性。

创建并行流非常简单。你可以使用以下两种方法之一:

  • 对于现有集合,可以使用 parallelStream() 方法。
  • 你可以通过在现有流上调用 parallel() 方法来使其并行。

何时使用并行流?

并行流可以在特定场景中提升性能,但它们并不总是最佳选择。以下是一些关键考虑因素:

  1. 适合的场景

    • 大数据集:当处理大量数据时,并行性效果最好。
    • CPU 密集型任务:对于大量使用 CPU 的计算任务(如数学运算或数据转换),并行流是理想选择。
  2. 避免使用并行流的场景

    • IO 密集型任务:如果你的任务涉及大量读写操作(如磁盘/网络操作),并行流可能不是最佳选择。
    • 小数据集:你需要记住,Java 虚拟机在底层仍然需要管理线程切换等操作。因此,在处理小数据集时,管理线程的开销可能会超过性能提升。

性能对比代码的解释

下面的代码将帮助我们通过求和操作来比较 Java 中顺序流和并行流的性能。我们将运行两个测试:

  • 第一个测试:范围从 1 到 1,000,000。
  • 第二个测试:范围从 1 到 100,000,000。

我们的主要目标是比较顺序流和并行流的处理时间,从而帮助我们理解使用并行流的优缺点。

int rangeLimit = 1_000_000;

long start = System.currentTimeMillis();
LongStream.rangeClosed(1, rangeLimit)
    .reduce(0L, Long::sum);
long end = System.currentTimeMillis();

System.out.println("Sequential Stream Time: " + (end - start) + " ms");

start = System.currentTimeMillis();
LongStream.rangeClosed(1, rangeLimit)
    .parallel()
    .reduce(0L, Long::sum);
end = System.currentTimeMillis();

System.out.println("Parallel Stream Time: " + (end - start) + " ms");

首先,我们创建了两个流:顺序流和并行流。并行流是通过在现有流上调用 .parallel() 方法创建的。两者都包含从 1 到 1,000,000 的数字,使用 LongStream.rangeClosed() 方法生成。

其次,我们对两个流执行了 .reduce(0L, Long::sum) 方法,该方法对输入流中的所有元素求和。由于 reduce 是一个终端操作,流会在调用该方法时立即开始处理。

我们能够测量此操作所花费的时间。这些信息通过 System.currentTimeMillis() 命令记录并存储在变量 startend 中。结果以毫秒为单位打印出来。

让我们执行代码两次,更新 rangeLimit 变量。第一次执行时,将其设置为 1,000,000,如代码所示。第二次执行时,将其设置为 100,000,000。

对于范围从 1 到 1,000,000:

Sequential Stream Time: 9 ms
Parallel Stream Time: 12 ms

我们可以看到,在这种情况下,并行流比顺序流稍慢。这是一个很好的例子,展示了对于像我们示例中使用的小数据集,管理多个线程可能会导致性能损失。

接下来,我们将范围增加到 100,000,000,结果如下:

Sequential Stream Time: 57 ms
Parallel Stream Time: 12 ms

最终,我们可以看到并行流的优势。在这里,并行流明显优于顺序流。较大的数据集能够通过利用多个 CPU 核心来加速计算过程。

重要注意事项:处理大数据集

我们需要记住一点:Java 中的 Long 类型的最大值是 2^63-1。因此,在我们的示例中,当我们测试较大的范围时,求和结果可能会超过此限制,从而导致不正确的结果。

由于本示例的主要目的是展示并行流的行为并比较效率,我们可以忽略结果可能不正确的事实。如果需要精确求和,你可能需要使用更大范围的类型,例如 BigInteger


3. Lambda 中的变量作用域

让我们简单讨论一下 Lambda。Lambda 表达式在 Stream API 中被广泛使用。老实说,我认为有很多开发者只在流中使用 Lambda。因此,我认为在本文中讨论一些与 Lambda 相关的点也是合理的。

我们应该意识到,Lambda 与变量的交互方式有其特殊性,作为 Java 开发者,理解 Lambda 如何捕获和使用变量至关重要。

让我们探讨一下变量作用域在 Lambda 表达式中是如何工作的,以及它与传统方法的区别。

在 Lambda 中捕获变量

假设你在 Lambda 的外部作用域中初始化了一个变量,并计划在 Lambda 函数中使用这个变量。你能这样做吗?

这取决于情况。我们只能使用从外部作用域捕获的变量,前提是它们是 final有效 final 的。那么,“有效 final”是什么意思呢?

简而言之,如果一个变量在初始化后其值从未改变,则它被认为是有效 final 的。因此,要在 Lambda 中使用变量,你有两种方法:

  1. 像往常一样初始化变量,并确保其值在初始化后不会改变。
  2. 在初始化时通过添加 final 关键字使变量成为 final
int factor = 2;  

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

numbers.stream()
    .map(n -> n * factor)  
    .forEach(System.out::println);

在上面的示例中,我们可以看到 factor 变量是有效 final 的,因为我们在初始化后没有更新它。这意味着这个变量可以在我们的 Lambda 中使用。你可以尝试在初始化后重新赋值 factor,看看会发生什么。


结论

Stream API 是一套强大且易于理解的工具,用于处理元素序列。如果正确使用,它可以帮助减少大量不必要的代码,使程序更具可读性,并提高应用程序的性能。但正如我所提到的,正确使用它以从性能和代码简洁性方面获得最佳结果至关重要。


【注】本文译自:Java Stream API: 3 Things Every Developer Should Know About

评估您的数据是否可用于人工智能的三个考虑因素

评估您的数据是否可用于人工智能的三个考虑因素

​ 多数组织正在人工智能和生成性人工智能的炒作中迷失方向。在许多情况下,他们并没有准备好人工智能项目所需的数据基础。三分之一的高管认为,只有不到50%的组织有了人工智能所需的数据,而多数组织并未准备好。因此,在开展人工智能项目之前,奠定正确的基础至关重要。在评估准备情况时,主要考虑因素如下:

  • 可用性:您的数据在哪里?
  • 类目:您将如何记录和协调您的数据?
  • 质量:优质数据是人工智能项目成功的关键。

​ 人工智能存在“垃圾进,垃圾出”的问题:如果您输入的数据质量差、不准确或无关紧要,那么输出也会如此。这些项目涉及的工作量和费用都非常高,风险也很大,因此从错误的数据开始是不可取的。

数据对人工智能的重要性

​ 数据是人工智能的基本要素;它是基于数据进行训练的,然后为特定目的处理数据。当您计划使用人工智能解决问题时——即使是使用现有的大型语言模型,如ChatGPT这样的生成性人工智能工具——您也需要为其提供业务的正确上下文(即优质数据),以便根据您的业务上下文定制答案(例如,用于检索增强生成)。而并不只是简单地将数据塞到模型中。

​ 如果您正在构建新模型,您必须知道将使用什么数据进行训练和验证。这些数据需要进行分离,以便您可以在一个数据集上进行训练,然后在不同的数据集上进行验证,来确定模型是否有效。

建立正确数据基础的挑战

​ 对于许多公司来说,知道数据在哪里以及数据的可用性是第一项重大挑战。如果您对自己的数据有一定的了解——数据的存在情况、数据所在的系统、数据的规则等——这已经是一个良好的起点。然而,事实是,许多公司并没有达到这种理解水平。

​ 数据并不总是随时可用;它可能分散在许多系统和信息孤岛中。尤其是大型公司,往往拥有非常复杂的数据环境。他们没有一个单一的、经过整理的数据库,所有模型所需的数据都整齐地组织在行和列中,可以直接检索和使用。

​ 另一个挑战是数据不仅存在于许多不同的系统中,而且格式各异。存在SQL数据库、NoSQL数据库、图数据库、数据湖,有时数据只能通过专有应用程序API访问。还有结构化数据和非结构化数据。一些数据存放在文件中,可能还有一些来自工厂传感器的实时数据,等等。根据您所在的行业,数据可能来自不同系统和格式的众多来源。协调这些数据是困难的;大多数组织没有相应的工具或系统来统一维护。

​ 即使您能够找到数据并将其转换为业务理解的统一格式(规范模型),您还需要考虑数据质量。数据是杂乱的;粗略看似乎没有问题,但仔细观察时,数据中会出现错误和重复,因为您是从多个系统中获取数据,不一致是不可避免的。您不能用低质量的训练数据来训练人工智能模型,然后期待高质量的结果。

如何奠定正确的基础:成功的三个步骤

​ 人工智能项目基础的第一块砖是了解您的数据。您必须能够清晰地表达业务正在捕获什么数据,这些数据存放在哪些系统中,数据的物理实现与业务的逻辑定义有何不同,以及业务规则是什么……

​ 接下来,您必须能够评估您的数据。就是要问:“对我的业务来说,什么是优质数据?”您需要定义优质数据的标准,并制定验证和清洗数据的规则,以及维护数据质量的策略。

​ 如果您能够从异构系统中获取数据并将其转换为规范模型,并对其进行整理以提高质量,您仍然需要关注可扩展性。这是第三个基础步骤。许多模型需要大量数据进行训练;您还需要大量数据用于检索增强生成,这是提高生成性人工智能模型性能的一种技术,它使用未包含在训练模型中的外部信息。所有这些数据都是不断变化和发展的。

​ 您需要一种方法来创建合适的数据管道,以适应您可能输入的数据的负载和体积。最初,您可能会被弄得不知所措,忙于寻找数据来源、清洗数据等,以至于没有充分考虑到对于不断演变的数据进行扩展将面临的挑战。因此,您必须考虑使用哪个平台来构建该项目,以便该平台能够扩展到您将引入的数据量。

为可信数据创造环境

​ 在进行人工智能项目时,将数据视为事后考虑因素必然会导致糟糕的商业结果。任何认真对待通过开发和使用人工智能来建立和维持商业优势的人都必须首先关注数据。主要问题在于:整理和准备用于商业目的数据具有相当的复杂性和挑战性,首当其冲的是时间因素。也就是说不给您范错的时间;最起码您要有一个帮助您维护高质量数据的平台和方法。了解和评估您的数据,然后规划可扩展性,您就会朝着更好的商业结果迈出一步。


【注】本文译自:https://sdtimes.com/ai/three-considerations-to-assess-your-datas-readiness-for-ai

使用Lambda表达式和接口的简单Java 8 Predicate示例

大量的Java编程涉及到对真或假值的评估,从条件语句到迭代循环。当您使用JDK的Streams API和Lambda函数时,可以使用备受欢迎的Java Predicate接口来简化布尔条件的评估。

也被称为Java 8 Predicate(源自引入函数式编程的JDK版本),这个简单的接口定义了五个方法,尽管只有Java Predicate的test方法在Stream或Lambda调用中被评估。

img

图1:Java 8 Predicate接口的五个方法的JavaDoc列表

传统的Java 8 Predicate示例:

尽管Java 8的Predicate是一个函数式接口,但开发人员仍可以以传统方式使用它。下面是一个Java Predicate示例,它简单地创建了一个扩展Predicate接口的新类,并在其主方法中使用Predicate的单独类:

import java.util.function.*;
public class Java8PredicateTutorial {  
  public static void main(String args[]) {
    PredicateExample example = new PredicateExample();
    System.out.printf("Gretzky's number is even: %s", example.test(99));
    boolean value = example.test(66);
    System.out.printf("nLemieux's number is even: %s ", value);  
  }   
}
class PredicateExample implements Predicate<Integer> {
  public boolean test(Integer x) {
    if (x%2==0){
      return true;
    } else {
    return false;
    }
  }
}

img

图2:如何编译和运行Java 8 Predicate示例

图2展示了编译和执行这个Predicate接口教程时的结果。

Java Predicate作为内部类:

如果您是一个喜欢内部类的开发人员,您可以对此进行一些简化,减少示例的冗长性。然而,这个Java 8 Predicate示例并不完全符合函数式编程的要求。

import java.util.function.*;
public class Java8PredicateTutorial {
  public static void main(String args[]) {
    Predicate predicateExample = new Predicate<Integer>() {
    public boolean test(Integer x) {
        return (x % 2 == 0);
        }
    };
    System.out.printf("Gretzky's number is even: %s", predicateExample.test(99));
    System.out.printf("nLemieux's number is even: %s ", predicateExample.test(66));
    }
}

Java Predicate lambda 示例

当然,如果您正在学习Java 8的Predicate接口,您很可能对如何在Lambda函数中使用它感兴趣。

Lambda表达式的目标是减少Java代码的冗长性,特别是在需要覆盖只有一个功能方法的接口的情况下。以下是使用Lambda表达式创建Java Predicate的代码示例:

Predicate<Integer> lambdaPredicate = (Integer x) -> (x % 2 == 0);

与传统的接口创建方法相比,毋庸置疑,Lambda表达式更加简洁。

以下是完整的使用Lambda表达式实现的Java Predicate示例:

import java.util.function.*;
public class Java8PredicateTutorial {
  public static void main(String args[]) {             
    /* Java predicate lambda example */
    Predicate<Integer> lambdaPredicate = (Integer x) -> (x % 2 == 0);             
    System.out.printf("Gretzky's number is even: %s", lambdaPredicate.test(99));
    System.out.printf("nLemieux's number is even: %s ", lambdaPredicate.test(66));
  }   
}

Java Predicate 和 lambda 流

自从JDK 8发布以来,函数式表达式已经在Java API中广泛应用。Streams API广泛使用Lambda表达式和Java Predicate,其中过滤表达式(filter expression)就是其中之一。下面是一个使用Lambda表达式、Stream和Predicate的示例,从一个Integer对象的列表中提取出所有的偶数:

import java.util.function.*;
import java.util.*;
import java.util.stream.*;
public class LambdaPredicateStreamExample {    
  public static void main(String args[]) {          
    List<Integer> jerseys = Arrays.asList(99, 66, 88, 16);
    /* Java predicate and lambda stream example usage */
    List<Integer> evenNumbers =
          jerseys.stream()
              .filter( x -> ((x%2)==0))
                  .collect(Collectors.toList());          
    /* The following line prints: [66, 88, 16] 8 */
    System.out.println(evenNumbers);
  }
}

正如您所看到的,Java的Lambda函数、流(Streams)和Predicate接口的组合使用可以创建非常紧凑的代码,既强大又易于阅读。


【注】本文译自:
Simple Java 8 Predicate example with lambda expressions and interfaces

Java开发中不要使用受检异常

简介

Types of Java Exception

Java是唯一(主流)实现了受检异常概念的编程语言。一开始,受检异常就是争议的焦点。在当时被视为一种创新概念(Java于1996年推出),如今却被视不良实践。

本文要讨论Java中非受检异常和受检异常的动机以及它们优缺点。与大多数关注这个主题的人不同,我希望提供一个平衡的观点,而不仅仅是对受检异常概念的批评。

我们先深入探讨Java中受检异常和非受检异常的动机。Java之父詹姆斯·高斯林对这个话题有何看法?接下来,我们要看一下Java中异常的工作原理以及受检异常存在的问题。我们还将讨论在何时应该使用哪种类型的异常。最后,我们将提供一些常见的解决方法,例如使用Lombok的@SneakyThrows注解。

Java和其他编程语言中异常的历史

在软件开发中,异常处理可以追溯到20世纪60年代LISP的引入。通过异常,我们可以解决在程序错误处理过程中可能遇到的几个问题。

异常的主要思想是将正常的控制流与错误处理分离。让我们看一个不使用异常的例子:

public void handleBookingWithoutExceptions(String customer, String hotel) {

 if (isValidHotel(hotel)) {

  int hotelId = getHotelId(hotel);

  if (sendBookingToHotel(customer, hotelId)) {

   int bookingId = updateDatabase(customer, hotel);

   if (bookingId > 0) {

    if (sendConfirmationMail(customer, hotel, bookingId)) {

     logger.log(Level.INFO, "Booking confirmed");

    } else {

     logger.log(Level.INFO, "Mail failed");

    }

   } else {

    logger.log(Level.INFO, "Database couldn't be updated");

   }

  } else {

   logger.log(Level.INFO, "Request to hotel failed");

  }

 } else {

  logger.log(Level.INFO, "Invalid data");

 }

}

程序的逻辑只占据了大约5行代码,其余的代码则是用于错误处理。这样,代码不再关注主要的流程,而是被错误检查所淹没。

如果我们的编程语言没有异常机制,我们只能依赖函数的返回值。让我们使用异常来重写我们的函数:

public void handleBookingWithExceptions(String customer, String hotel) {

 try {

  validateHotel(hotel);

  sendBookingToHotel(customer, getHotelId(hotel));

  int bookingId = updateDatabase(customer, hotel);

  sendConfirmationMail(customer, hotel, bookingId);

  logger.log(Level.INFO, "Booking confirmed");

 } catch(Exception e) {

  logger.log(Level.INFO, e.getMessage());

 }

}

采用这种方法,我们不需要检查返回值,而是将控制流转移到catch块中。这样的代码更易读。我们有两个独立的流程: 正常流程和错误处理流程。

除了可读性之外,异常还解决了"半谓词问题"(semipredicate problem)。简而言之,半谓词问题发生在表示错误(或不存在值)的返回值成为有效返回值的情况下。让我们看几个示例来说明这个问题:

示例:

int index = "Hello World".indexOf("World");

int value = Integer.parseInt("123");

int freeSeats = getNumberOfAvailableSeatsOfFlight();

indexOf() 方法如果未找到子字符串,将返回 -1。当然,-1 绝对不可能是一个有效的索引,所以这里没有问题。然而,parseInt() 方法的所有可能返回值都是有效的整数。这意味着我们没有一个特殊的返回值来表示错误。最后一个方法 getNumberOfAvailableSeatsOfFlight() 可能会导致隐藏的问题。我们可以将 -1 定义为错误或没有可用信息的返回值。乍看起来这似乎是合理的。然而,后来可能发现负数表示等待名单上的人数。异常机制能更优雅地解决这个问题。

Java中异常的工作方式

在讨论是否使用受检异常之前,让我们简要回顾一下Java中异常的工作方式。下图显示了异常的类层次结构:

java-exception

RuntimeException继承自Exception,而Error继承自Throwable。RuntimeException和Error被称为非受检异常,意味着它们不需要由调用代码处理(即它们不需要被“检查”)。所有其他继承自Throwable(通常通过Exception)的类都是受检异常,这意味着编译器期望调用代码处理它们(即它们必须被“检查”)。

所有继承自Throwable的异常,无论是受检的还是非受检的,都可以在catch块中捕获。

最后,值得注意的是,受检异常和非受检异常的概念是Java编译器的特性。JVM本身并不知道这个区别,所有的异常都是非受检的。这就是为什么其他JVM语言不需要实现这个特性的原因。

在我们开始讨论是否使用受检异常之前,让我们简要回顾一下这两种异常类型之间的区别。

受检异常

受检异常需要被try-catch块包围,或者调用方法需要在其签名中声明异常。由于Scanner类的构造函数抛出一个FileNotFoundException异常,这是一个受检异常,所以下面的代码无法编译:

public void readFile(String filename) {

 Scanner scanner = new Scanner(new File(filename));

}

我们会得到一个编译错误:

Unhandled exception: java.io.FileNotFoundException

我们有两种选项来解决这个问题。我们可以将异常添加到方法的签名中:

public void readFile(String filename) throws FileNotFoundException {

 Scanner scanner = new Scanner(new File(filename));

}

或者我们可以使用try-catch块在现场处理异常:

public void readFile(String filename) {

 try {

  Scanner scanner = new Scanner(new File(filename));

 } catch (FileNotFoundException e) {

  // handle exception

 }

}

非受检异常

对于非受检异常,我们不需要做任何处理。由Integer.parseInt引发的NumberFormatException是一个运行时异常,所以下面的代码可以编译通过:

public int readNumber(String number) {

 return Integer.parseInt(callEndpoint(number));

}

然而,我们仍然可以选择处理异常,因此以下代码也可以编译通过:

public int readNumber(String number) {

 try {

  return Integer.parseInt(callEndpoint(number));

 } catch (NumberFormatException e) {

  // handle exception

  return 0;

 }

}

为什么我们要使用受检异常?

如果我们想了解受检异常背后的动机,我们需要看一下Java的历史。该语言的创建是以强调健壮性和网络功能为重点的。

最好用Java创始人詹姆斯·高斯林(James Gosling)自己的一句话来表达:“你不能无意地说,‘我不在乎。’你必须明确地说,‘我不在乎。’”这句话摘自一篇与詹姆斯·高斯林进行的有趣的采访,在采访中他详细讨论了受检异常。

在《编程之父》这本书中,詹姆斯也谈到了异常。他说:“人们往往忽略了检查返回代码。”

这再次强调了受检异常的动机。通常情况下,当错误是由于编程错误或错误的输入时,应该使用非受检异常。如果在编写代码时程序员无法做任何处理,应该使用受检异常。后一种情况的一个很好的例子是网络问题。开发人员无法解决这个问题,但程序应该适当地处理这种情况,可以是终止程序、重试操作或简单地显示错误消息。

受检异常存在的问题

了解了受检异常和非受检异常背后的动机,我们再来看看受异常在代码库中可能引入的一些问题。

受检异常不适应规模化

一个主要反对受异常的观点是代码的可扩展性和可维护性。当一个方法的异常列表发生变化时,会打破调用链中从调用方法开始一直到最终实现try-catch来处理异常的方法的所有方法调用。举个例子,假设我们调用一个名为libraryMethod()的方法,它是外部库的一部分:

public void retrieveContent() throws IOException {

 libraryMethod();

}

在这里,方法libraryMethod()本身来自一个依赖项,例如,一个处理对外部系统进行REST调用的库。其实现可能如下所示:

public void libraryMethod() throws IOException {

 // some code

}

在将来,我们决定使用库的新版本,甚至用另一个库替换它。尽管功能相似,但新库中的方法会抛出两个异常:

public void otherSdkCall() throws IOException, MalformedURLException {

 // call method from SDK

}

由于有两个受检异常,我们的方法声明也需要更改:

public void retrieveContent() throws IOException, MalformedURLException {

 sdkCall();

}

对于小型代码库来说,这可能不是一个大问题,但对于大型代码库来说,这将需要进行相当多的重构。当然,我们也可以直接在方法内部处理异常:

public void retrieveContent() throws IOException {

 try {

  otherSdkCall();

 } catch (MalformedURLException e) {

  // do something with the exception

 }

}

使用这种方法,我们在代码库中引入了一种不一致性,因为我们立即处理了一个异常,而推迟了另一个异常的处理。

异常传播

一个与可扩展性非常相似的论点是受检异常如何在调用堆栈中传播。如果我们遵循“尽早抛出,尽晚捕获”的原则,我们需要在每个调用方法上添加一个throws子句(a):

异常传播

相反,非受检异常(b)只需要在实际发生异常的地方声明一次,并在我们希望处理异常的地方再次声明。它们会在调用堆栈中自动传播,直到达到实际处理异常的位置。

不必要的依赖关系

受检异常还会引入与非受检异常不必要的依赖关系。让我们再次看看在场景(a)中我们在三个不同的位置添加了IOException。如果methodA()、methodB()和methodC()位于不同的类中,那么所有相关类都将对异常类有一个依赖关系。如果我们使用了非受检异常,我们只需要在methodA()和methodC()中有这个依赖关系。甚至methodB()所在的类或模块都不需要知道异常的存在。

让我们用一个例子来说明这个想法。假设你从度假回家。你在酒店前台退房,乘坐公共汽车去火车站,然后换乘一次火车,在回到家乡后,你又乘坐另一辆公共汽车从车站回家。回到家后,你意识到你把手机忘在了酒店里。在你开始整理行李之前,你进入了“异常”流程,乘坐公共汽车和火车回到酒店取手机。在这种情况下,你按照之前相反的顺序做了所有的事情(就像在Java中发生异常时向上移动堆栈跟踪一样),直到你到达酒店。显然,公共汽车司机和火车操作员不需要知道“异常”,他们只需要按照他们的工作进行。只有在前台,也就是“回家”流程的起点,我们需要询问是否有人找到了手机。

糟糕的编码实践

当然,作为专业的软件开发人员,我们绝不能在良好的编码实践上选择方便。然而,当涉及到受检异常时,往往会诱使我们快速引入以下三种模式。通常的想法是以后再处理。我们都知道这样的结果。另一个常见的说法是“我想为正常流程编写代码,不想被异常打扰”。我经常见到以下三种模式。

第一种模式是捕获所有异常(catch-all exception):

public void retrieveInteger(String endpoint) {

 try {

  URL url = new URL(endpoint);

  int result = Integer.parseInt(callEndpoint(endpoint));

 } catch (Exception e) {

  // do something with the exception

 }

}

我们只是捕获所有可能的异常,而不是单独处理不同的异常:

public void retrieveInteger(String endpoint) {

 try {

  URL url = new URL(endpoint);

  int result = Integer.parseInt(callEndpoint(endpoint));

 } catch (MalformedURLException e) {

  // do something with the exception

 } catch (NumberFormatException e) {

  // do something with the exception

 }

}

当然,在一般情况下,这并不一定是一种糟糕的实践。如果我们只想记录异常,或者在Spring Boot的@ExceptionHandler中作为最后的安全机制,这是一种适当的做法。

第二种模式是空的catch块:

public void myMethod() {

 try {

  URL url = new URL("malformed url");

 } catch (MalformedURLException e) {}

}

这种方法显然绕过了受检异常的整个概念。它完全隐藏了异常,使我们的程序在没有提供任何关于发生了什么的信息的情况下继续执行。

第三种模式是简单地打印堆栈跟踪并继续执行,就好像什么都没有发生一样:

public void consumeAndForgetAllExceptions(){

 try {

  // some code that can throw an exception

 } catch (Exception ex){

  ex.printStacktrace();

 }

}

为了满足方法签名而添加额外的代码

有时我们可以确定除非出现编程错误,否则不会抛出异常。让我们考虑以下示例:

public void readFromUrl(String endpoint) {

 try {

  URL url = new URL(endpoint);

 } catch (MalformedURLException e) {

  // do something with the exception

 }

}

MalformedURLException是一个受检异常,当给定的字符串不符合有效的URL格式时,会抛出该异常。需要注意的重要事项是,如果URL格式不正确,就会抛出异常,这并不意味着URL实际上存在并且可以访问。

即使我们在之前验证了格式:

public void readFromUrl(@ValidUrl String endpoint)

或者我们已经将其硬编码:

public static final String endpoint = "http://www.example.com";

编译器仍然强制我们处理异常。我们需要写两行“无用”的代码,只是因为有一个受检异常。

如果我们无法编写代码来触发某个异常的抛出,就无法对其进行测试,因此测试覆盖率将会降低。

有趣的是,当我们想将字符串解析为整数时,并不强制我们处理异常:

Integer.parseInt("123");

parseInt方法在提供的字符串不是有效整数时会抛出NumberFormatException,这是一个非受检异常。

Lambda表达式和异常

受检异常并不总是与Lambda表达式很好地配合使用。让我们来看一个例子:

public class CheckedExceptions {

 public static String readFirstLine(String filename) throws FileNotFoundException {

  Scanner scanner = new Scanner(new File(filename));

  return scanner.next();

 }

 public void readFile() {

  List<String> fileNames = new ArrayList<>();

  List<String> lines = fileNames.stream().map(CheckedExceptions::readFirstLine).toList();

 }

}

由于我们的readFirstLine()方法抛出了一个受检异常,所以会导致编译错误:

Unhandled exception: java.io.FileNotFoundException in line 8.

如果我们尝试使用try-catch块来修正代码:

public void readFile() {

 List<String> fileNames = new ArrayList<>();

 try {

  List<String> lines = fileNames.stream()

    .map(CheckedExceptions::readFirstLine)

    .toList();

 } catch (FileNotFoundException e) {

   // handle exception

 }

}

我们仍然会得到一个编译错误,因为我们无法在lambda内部将受检异常传播到外部。我们必须在lambda表达式内部处理异常并抛出一个运行时异常:

public void readFile() {

 List<String> lines = fileNames.stream()

  .map(filename -> {

   try{

    return readFirstLine(filename);

   } catch(FileNotFoundException e) {

    throw new RuntimeException("File not found", e);

   }

  }).toList();

}

不幸的是,如果静态方法引用抛出受检异常,这种方式将变得不可行。或者,我们可以让lambda表达式返回一个错误消息,然后将其添加到结果中:

public void readFile() {

 List<String> lines = fileNames.stream()

  .map(filename -> {

   try{

    return readFirstLine(filename);

   } catch(FileNotFoundException e) {

    return "default value";

   }

  }).toList();

}

然而,代码看起来仍然有些杂乱。

我们可以在lambda内部传递一个非受检异常,并在调用方法中捕获它:

public class UncheckedExceptions {

 public static int parseValue(String input) throws NumberFormatException {

  return Integer.parseInt(input);

 }

 public void readNumber() {

  try {

   List<String> values = new ArrayList<>();

   List<Integers> numbers = values.stream()

       .map(UncheckedExceptions::parseValue)

       .toList();

  } catch(NumberFormatException e) {

   // handle exception

  }

 }

}

在这里,我们需要注意之前使用受检异常和使用非受检异常的例子之间的一个关键区别。对于非受检异常,流的处理将继续到下一个元素,而对于受检异常,处理将结束,并且不会处理更多的元素。显然,我们想要哪种行为取决于我们的用例。

处理受检异常的替代方法

将受检异常包装为非受检异常

我们可以通过将受检异常包装为非受检异常来避免在调用堆栈中的所有方法中添加throws子句。而不是让我们的方法抛出一个受检异常:

public void myMethod() throws IOException{}

我们可以将其包装在一个非受检异常中:

public void myMethod(){

 try {

  // some logic

 } catch(IOException e) {

  throw new MyUnchckedException("A problem occurred", e);

 }

}

理想情况下,我们应用异常链。这样可以确保原始异常不会被隐藏。我们可以在第5行看到异常链的应用,原始异常作为参数传递给新的异常。这种技术在早期版本的Java中几乎适用于所有核心Java异常。

异常链是许多流行框架(如Spring或Hibernate)中常见的一种方法。这两个框架从受检异常转向非受检异常,并将不属于框架的受检异常包装在自己的运行时异常中。一个很好的例子是Spring的JDBC模板,它将所有与JDBC相关的异常转换为Spring框架的非受检异常。

Lombok @SneakyThrows

Project Lombok为我们提供了一个注解,可以消除异常链的需要。而不是在我们的方法中添加throws子句:

public void beSneaky() throws MalformedURLException {

 URL url = new URL("http://test.example.org");

}

我们可以添加@SneakyThrows 注解,这样我们的代码就可以编译通过:

@SneakyThrows

public void beSneaky() {

 URL url = new URL("http://test.example.org");

}

然而,重要的是要理解,@SneakyThrows并不会使MalformedURLException的行为完全像运行时异常一样。我们将无法再捕获它,并且以下代码将无法编译:

public void callSneaky() {

 try {

  beSneaky();

 } catch (MalformedURLException e) {

  // handle exception

 }

}

由于@SneakyThrows移除了异常,而MalformedURLException仍然被视为受检异常,因此我们将在第4行得到编译器错误:

Exception 'java.net.MalformedURLException' is never thrown in the corresponding try block

性能

在我的研究过程中,我遇到了一些关于异常性能的讨论。在受检异常和非受检异常之间是否存在性能差异?实际上,它们之间没有性能差异。这是一个在编译时决定的特性。

然而,是否在异常中包含完整的堆栈跟踪会导致显着的性能差异:

public class MyException extends RuntimeException {

 public MyException(String message, boolean includeStacktrace) {

  super(message, null, !includeStacktrace, includeStacktrace);

 }

}

在这里,我们在自定义异常的构造函数中添加了一个标志。该标志指定是否要包含完整的堆栈跟踪。在抛出异常的情况下,构建堆栈跟踪会导致程序变慢。因此,如果性能至关重要,则应排除堆栈跟踪。

一些指南

如何处理软件中的异常是我们工作的一部分,它高度依赖于具体的用例。在我们结束讨论之前,这里有三个高级指南,我相信它们(几乎)总是正确的。

  • 如果不是编程错误,或者程序可以执行一些有用的恢复操作,请使用受检异常。
  • 如果是编程错误,或者程序无法进行任何恢复操作,请使用运行时异常。
  • 避免空的catch块。

结论

本文深入探讨了Java中的异常。我们讲了为什么要引入异常到语言中,何时应该使用受检异常和非受检异常。我们还讨论了受检异常的缺点以及为什么它们现在被认为是不良实践 – 尽管也有一些例外情况。


【注】本文译自: Don’t Use Checked Exceptions (reflectoring.io)