Java
Um núcleo mínimo de Java com sintaxe, tipos e semântica que cabem numa página.
Centro de Informática, UFPE Use as setas, Espaço ou PageUp/PageDown. F para tela cheia.
Dois jeitos de estudar objetos no TAPL
Capítulo 18: codificar
Objetos, classes e herança são montados com peças do lambda-cálculo: registros, referências e subtipagem.
Mostra o mecanismo por baixo, mas o objeto deixa de ser visível como conceito.
Capítulo 19: tratar como primitivo
Classes e objetos são construções da própria linguagem, com regras de tipo e de avaliação próprias.
É o caminho do Featherweight Java (FJ), proposto por Igarashi, Pierce e Wadler em 1999.
Completude contra compacidade
Um modelo formal serve para descrever uma linguagem com precisão e provar propriedades dela, como segurança de tipos.
Quanto mais recursos o modelo cobre, mais pesada fica a prova. FJ escolhe o outro extremo: corta tudo que alonga a prova sem mudar o argumento.
O resultado é útil para estender Java no papel: genéricos (GJ), raw types, classes internas e compilação que preserva tipos foram estudados sobre FJ.
Inside every large language is a small language struggling to get out. Igarashi, Pierce e Wadler (1999)
O que FJ mantém de Java e o que corta
Mantém
- Classes mutuamente recursivas
- Criação de objetos e acesso a campos
- Chamada e sobrescrita de métodos
- Recursão aberta via
this - Subtipagem e cast
Corta
- Atribuição: campos só são definidos no construtor
- Interfaces, sobrecarga,
super.m(),null - Tipos base (
int,bool), exceções - Classes internas, métodos abstratos,
public/private - Concorrência e reflexão
Sem atribuição, FJ é um fragmento funcional de Java. Mesmo assim é Turing-completo: dá para codificar o lambda-cálculo nele.
Um programa é uma tabela de classes mais um termo
class A extends Object { A() { super(); } } class B extends Object { B() { super(); } } class Pair extends Object { Object fst; Object snd; // construtor Pair(Object fst, Object snd) { super(); this.fst=fst; this.snd=snd; } // método Pair setfst(Object newfst) { return new Pair(newfst, this.snd); } }
Convenções rígidas, para a sintaxe ficar regular:
extendssempre escrito, mesmo paraObject- Construtor sempre presente: um parâmetro por campo,
super(...)e depoisthis.f=f - Receptor sempre explícito:
this.snd, nuncasnd - Corpo de método é só
return t;
super e = só aparecem dentro do construtor.
A linguagem inteira tem cinco formas de termo
| Forma | Gramática | No exemplo |
|---|---|---|
| Variável | x | newfst, this |
| Acesso a campo | t.f | this.snd |
| Chamada de método | t.m(t) | new Pair(...).setfst(new B()) |
| Criação de objeto | new C(t) | new A(), new Pair(..., ...) |
| Cast | (C) t | (Pair) t.fst |
A barra em t indica sequência: t1, ..., tn.
this é tratado como variável, não palavra-chave. Ele fica implicitamente ligado em todo corpo de método.
A gramática completa
Valores são só objetos com todos os argumentos já avaliados. "Tudo é objeto", como no lambda-cálculo "tudo é função".
Um programa é o par (CT, t): uma tabela de classes e o termo a avaliar, que faz o papel do main.
Metavariáveis: A..E classes, f e g campos, m métodos, x parâmetros, s e t termos, u e v valores.
Em Java, o nome do tipo importa
Estrutural
NatPair = {fst:Nat, snd:Nat}
O nome é só abreviação. Subtipagem é decidida comparando a forma dos tipos.
Mais elegante para provas e para recursos avançados (polimorfismo, tipos abstratos). Domina a literatura.
Nominal
class Pair extends Object { ... }
Todo tipo tem nome, e subtipagem é a que o programador declarou com extends.
- O nome vira etiqueta em tempo de execução: permite cast e
instanceof - Tipos recursivos saem de graça
- Checar
C <: Dé quase trivial
Subtipagem sai direto das declarações extends
A tabela de classes CT mapeia cada nome de classe para sua declaração.
Object é especial: não está na tabela, não tem campos nem métodos.
Condições de sanidade da tabela:
- toda classe citada existe em CT
Objectnão está em CT- não há ciclo em
<:
Quatro consultas à tabela de classes
| Função | Devolve | Como |
|---|---|---|
| fields(C) | todos os campos C f, herdados primeiro | campos de D (superclasse) seguidos dos de C; fields(Object) é vazio |
| mtype(m, C) | tipo B → B | procura m em C; se não achar, sobe para a superclasse |
| mbody(m, C) | par (x, t): parâmetros e corpo | mesma busca subindo a hierarquia: é aqui que mora a sobrescrita |
| override(m, D, C→C0) | sobrescrita válida? | se m existe em D, o tipo tem que ser idêntico |
Exemplo: fields(Pair) = Object fst, Object snd e mbody(setfst, Pair) = (newfst, new Pair(newfst, this.snd)).
fields(Cachorro) = • · mtype(falar,Cachorro) = [] → Som · mbody(falar,Cachorro) = ([], new Latido())Duas famílias de regras, dois papéis diferentes
Faz o passo real: campo, método ou cast.
Leva a avaliação até o próximo subtermo.
t ⟶ t': “t reduz para t' em um passo”.Como buscar um campo de um objeto?
Conclusão: o formato padronizado do construtor permite projetar por posição.
Um cast muda o objeto?
Conclusão: o cast só verifica a hierarquia; não altera o objeto.
O construtor externo já é um valor?
.fst vira new A()Conclusão: call-by-value avalia os argumentos antes de considerar new C(...) um valor.
Por que o exemplo do livro precisa de cast?
new Pair(new Pair(new A(),new B()), new A()).fstclasse real: Pairfst : Objectfields(Object) é vazio; então .snd não tipa.
((Pair) ... .fst).sndO cast torna o receptor tipável como Pair; em runtime ele confere a classe real.
Conclusão: o tipo estático vem da declaração do campo; a classe real vem do valor armazenado.
setfst: buscar o corpo e substituir
newfst↦new B()this↦receptorthis recebe receptor.this.snd vira new B().Qual corpo de falar() executa?
class Animal extends Object { Som falar() { return new Som(); } }
class Cachorro extends Animal { Som falar() { return new Latido(); } }
new Cachorro().falar()⟶new Latido()mbody(falar, Cachorro)encontra primeiro o método de CachorroConclusão: a classe real do receptor decide onde a busca começa; a sobrescrita de Cachorro vence.
Três jeitos de ficar stuck
new A().fst
fields(A) é vazio; E-ProjNew não se aplica.
new A().setfst(new B())
mbody(setfst,A) é indefinido; E-InvkNew não se aplica.
(Gato)new Cachorro()
Cachorro <: Gato é falso; E-CastNew não se aplica.
Tipagem elimina campo e método inexistentes. Um cast preso continua sendo a exceção de progresso.
Uma regra de tipo para cada forma de termo
Lê-se Γ ⊢ t : C como "no ambiente Γ, o termo t tem tipo C". Ex.: Γ ⊢ new Gato() : Gato.
Não há subsunção solta: o <: aparece onde é preciso. Cada termo tem um tipo mínimo único.
Cast tem três regras, e uma delas é "estúpida"
upcast
downcast
stupid cast
O compilador Java rejeita cast entre classes não relacionadas. Por que FJ aceita?
Porque um termo bem tipado pode virar um assim durante a avaliação:
(A)(Object)new B() // T-DCast: ok ⟶ (A)new B() // só T-SCast tipa
Sem T-SCast, a preservação de tipos seria falsa. Uma tipagem FJ só corresponde a uma tipagem Java se não usar essa regra.
(Animal)new Gato()T-UCast · sempre seguro(Gato)(Animal)new Gato()T-DCast · checa em runtime(Cachorro)new Gato()T-SCast · classes irmãsMétodos e classes também são verificados
CT(C) = class C extends D {...} override(m, D, C→C0)
fields(D) = D g M OK in C
- Método: o corpo é tipado com os parâmetros e com
this : C. O resultado pode ser subtipo do declarado. - Sobrescrita exige o mesmo tipo da superclasse.
- Classe: o construtor tem que passar os campos herdados para
supere inicializar os próprios. - Sem circularidade: os tipos dos métodos são declarados, então a ordem não importa.
Bem tipado não trava, exceto em downcast
Preservação
O tipo pode ficar mais específico durante a avaliação, nunca diferente.
Exemplo: (Object)new A() tem tipo Object e avança para new A(), de tipo A.
Progresso
A única forma de travar é um downcast que falha. Campo e método inexistentes nunca acontecem.
Se o programa só tem upcasts, ele nunca trava. Em Java completo, o cast que falha vira exceção.
Três perguntas que ajudam a fixar o modelo
| Exerc. | Pergunta | Ideia da resposta |
|---|---|---|
| 19.4.1 | Por que não há regra dizendo que Object é supertipo de tudo, como S-Top? | Toda classe declara uma superclasse e não há ciclos, então toda cadeia de extends termina em Object. A transitividade já dá C <: Object, e os únicos tipos são classes. |
| 19.4.3 | Como adicionar atribuição a campos? | Seguir o capítulo 13: introduzir locações e uma memória (heap). Objetos passam a ser referências, e a avaliação deixa de acontecer só na sintaxe. |
| 19.4.7 | Como adicionar super.m()? | A busca do corpo precisa começar na superclasse da classe onde o método foi escrito, não na do receptor. O corpo tem que lembrar a classe que o definiu. |
As duas visões se complementam
Codificação (cap. 18)
- Expõe os mecanismos de encapsulamento e reuso
- Mostra como um compilador traduz objetos para algo mais baixo nível
Primitivo (cap. 19)
- Fala de tipos e avaliação de objetos diretamente
- Melhor ferramenta para projetar e documentar uma linguagem
Featherweight Java em uma página
- Cinco termos: variável, campo, método,
newe cast - Subtipagem nominal, vinda direto de
extends - Três regras de computação e avaliação por valor, sem heap
- Três regras de cast, incluindo o "stupid cast" para a preservação funcionar
- Segurança: bem tipado só trava em downcast
Igarashi, A.; Pierce, B. C.; Wadler, P. Featherweight Java: a minimal core calculus for Java and GJ. OOPSLA 1999; ACM TOPLAS 23(3), 2001.